Apply inbounds on nodes via Xray with Reality settings and client sync.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -117,6 +117,7 @@ func (s *Server) clientsCreate(w http.ResponseWriter, r *http.Request) {
|
||||
formErr("Не удалось создать (возможно username занят)")
|
||||
return
|
||||
}
|
||||
s.syncNodesForInbounds(inboundIDs)
|
||||
http.Redirect(w, r, "/admin/clients/"+c.ID.String()+"?ok=created", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -187,11 +188,16 @@ func (s *Server) clientUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/clients/"+id.String()+"?err=username", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
oldIDs, _ := db.ListClientInboundIDs(s.DB, id)
|
||||
if err := db.UpdateClient(s.DB, c, parseInboundIDs(r)); err != nil {
|
||||
log.Printf("update client: %v", err)
|
||||
http.Redirect(w, r, "/admin/clients/"+id.String()+"?err=save", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
newIDs := parseInboundIDs(r)
|
||||
merged := append([]uuid.UUID{}, oldIDs...)
|
||||
merged = append(merged, newIDs...)
|
||||
s.syncNodesForInbounds(merged)
|
||||
http.Redirect(w, r, "/admin/clients/"+id.String()+"?ok=saved", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -211,6 +217,7 @@ func (s *Server) clientToggle(w http.ResponseWriter, r *http.Request) {
|
||||
next = models.ClientStatusActive
|
||||
}
|
||||
_ = db.SetClientStatus(s.DB, id, next)
|
||||
s.syncNodesForInbounds(c.InboundIDs)
|
||||
http.Redirect(w, r, "/admin/clients/"+id.String()+"?ok=status", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -220,6 +227,12 @@ func (s *Server) clientDelete(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
c, _ := db.GetClient(s.DB, id)
|
||||
var inboundIDs []uuid.UUID
|
||||
if c != nil {
|
||||
inboundIDs = c.InboundIDs
|
||||
}
|
||||
_ = db.DeleteClient(s.DB, id)
|
||||
s.syncNodesForInbounds(inboundIDs)
|
||||
http.Redirect(w, r, "/admin/clients?ok=deleted", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -88,6 +88,8 @@ func (s *Server) Routes() http.Handler {
|
||||
r.HandleFunc("/admin/profiles/{id}", s.Auth.RequireAuth(s.profileUpdate)).Methods(http.MethodPost)
|
||||
r.HandleFunc("/admin/profiles/{id}/delete", s.Auth.RequireAuth(s.profileDelete)).Methods(http.MethodPost)
|
||||
r.HandleFunc("/admin/profiles/{id}/inbounds", s.Auth.RequireAuth(s.inboundCreate)).Methods(http.MethodPost)
|
||||
r.HandleFunc("/admin/profiles/{id}/inbounds/{inboundID}", s.Auth.RequireAuth(s.inboundEditGet)).Methods(http.MethodGet)
|
||||
r.HandleFunc("/admin/profiles/{id}/inbounds/{inboundID}", s.Auth.RequireAuth(s.inboundUpdate)).Methods(http.MethodPost)
|
||||
r.HandleFunc("/admin/profiles/{id}/inbounds/{inboundID}/toggle", s.Auth.RequireAuth(s.inboundToggle)).Methods(http.MethodPost)
|
||||
r.HandleFunc("/admin/profiles/{id}/inbounds/{inboundID}/delete", s.Auth.RequireAuth(s.inboundDelete)).Methods(http.MethodPost)
|
||||
|
||||
|
||||
+190
-35
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/orohi/vpn-panel/internal/db"
|
||||
"github.com/orohi/vpn-panel/internal/models"
|
||||
"github.com/orohi/vpn-panel/internal/xraykeys"
|
||||
)
|
||||
|
||||
func (s *Server) profilesList(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -119,6 +120,95 @@ func (s *Server) profileDelete(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/profiles?ok=deleted", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func parseInboundForm(r *http.Request, profileID, protocolID uuid.UUID, proto *models.Protocol) (*models.Inbound, error) {
|
||||
port, _ := strconv.Atoi(r.FormValue("port"))
|
||||
if port <= 0 {
|
||||
port = proto.Port
|
||||
if port <= 0 {
|
||||
port = 443
|
||||
}
|
||||
}
|
||||
tag := strings.TrimSpace(r.FormValue("tag"))
|
||||
if tag == "" {
|
||||
tag = strings.ToUpper(proto.Code) + "-" + strconv.Itoa(port)
|
||||
}
|
||||
sortOrder, _ := strconv.Atoi(r.FormValue("sort_order"))
|
||||
network := r.FormValue("network")
|
||||
security := r.FormValue("security")
|
||||
flow := strings.TrimSpace(r.FormValue("flow"))
|
||||
if security == "reality" && network == "tcp" && flow == "" && proto.Code == "vless" {
|
||||
flow = "xtls-rprx-vision"
|
||||
}
|
||||
in := &models.Inbound{
|
||||
ProfileID: profileID,
|
||||
Tag: tag,
|
||||
ProtocolID: protocolID,
|
||||
ProtocolCode: proto.Code,
|
||||
Port: port,
|
||||
Network: network,
|
||||
Security: security,
|
||||
Listen: strings.TrimSpace(r.FormValue("listen")),
|
||||
Enabled: true,
|
||||
SortOrder: sortOrder,
|
||||
Remark: strings.TrimSpace(r.FormValue("remark")),
|
||||
Path: strings.TrimSpace(r.FormValue("path")),
|
||||
Host: strings.TrimSpace(r.FormValue("host")),
|
||||
SNI: strings.TrimSpace(r.FormValue("sni")),
|
||||
Fingerprint: strings.TrimSpace(r.FormValue("fingerprint")),
|
||||
Flow: flow,
|
||||
ALPN: strings.TrimSpace(r.FormValue("alpn")),
|
||||
RealityDest: strings.TrimSpace(r.FormValue("reality_dest")),
|
||||
RealityServerNames: strings.TrimSpace(r.FormValue("reality_server_names")),
|
||||
RealityPrivateKey: strings.TrimSpace(r.FormValue("reality_private_key")),
|
||||
RealityPublicKey: strings.TrimSpace(r.FormValue("reality_public_key")),
|
||||
RealityShortID: strings.TrimSpace(r.FormValue("reality_short_id")),
|
||||
SSMethod: strings.TrimSpace(r.FormValue("ss_method")),
|
||||
Password: strings.TrimSpace(r.FormValue("password")),
|
||||
}
|
||||
if in.Security == "reality" {
|
||||
priv, pub, sid, dest, names, err := xraykeys.EnsureReality(
|
||||
in.RealityPrivateKey, in.RealityPublicKey, in.RealityShortID,
|
||||
in.RealityDest, in.RealityServerNames,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
in.RealityPrivateKey, in.RealityPublicKey, in.RealityShortID = priv, pub, sid
|
||||
in.RealityDest, in.RealityServerNames = dest, names
|
||||
if in.SNI == "" {
|
||||
in.SNI = strings.Split(names, ",")[0]
|
||||
}
|
||||
if in.Fingerprint == "" {
|
||||
in.Fingerprint = "chrome"
|
||||
}
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func (s *Server) syncProfileNodes(profileID uuid.UUID) {
|
||||
nodes, _ := db.ListNodes(s.DB)
|
||||
for _, n := range nodes {
|
||||
if n.ProfileID != nil && *n.ProfileID == profileID {
|
||||
_ = db.ApplyProfileInboundsToNodeProtocols(s.DB, n.ID)
|
||||
if n.Status == models.NodeStatusOnline {
|
||||
nn := n
|
||||
_ = s.syncNodeConfig(&nn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) syncNodesForInbounds(inboundIDs []uuid.UUID) {
|
||||
nodes, err := db.ListOnlineNodesByInboundIDs(s.DB, inboundIDs)
|
||||
if err != nil {
|
||||
log.Printf("list nodes for inbounds: %v", err)
|
||||
return
|
||||
}
|
||||
for i := range nodes {
|
||||
_ = s.syncNodeConfig(&nodes[i])
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) inboundCreate(w http.ResponseWriter, r *http.Request) {
|
||||
profileID, err := uuid.Parse(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
@@ -136,37 +226,17 @@ func (s *Server) inboundCreate(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?err=protocol", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
port, _ := strconv.Atoi(r.FormValue("port"))
|
||||
if port <= 0 {
|
||||
port = proto.Port
|
||||
if port <= 0 {
|
||||
port = 443
|
||||
}
|
||||
}
|
||||
tag := strings.TrimSpace(r.FormValue("tag"))
|
||||
if tag == "" {
|
||||
tag = strings.ToUpper(proto.Code) + "-" + strconv.Itoa(port)
|
||||
}
|
||||
sortOrder, _ := strconv.Atoi(r.FormValue("sort_order"))
|
||||
in := &models.Inbound{
|
||||
ProfileID: profileID,
|
||||
Tag: tag,
|
||||
ProtocolID: protocolID,
|
||||
ProtocolCode: proto.Code,
|
||||
Port: port,
|
||||
Network: r.FormValue("network"),
|
||||
Security: r.FormValue("security"),
|
||||
Listen: strings.TrimSpace(r.FormValue("listen")),
|
||||
Enabled: true,
|
||||
SortOrder: sortOrder,
|
||||
Remark: strings.TrimSpace(r.FormValue("remark")),
|
||||
in, err := parseInboundForm(r, profileID, protocolID, proto)
|
||||
if err != nil {
|
||||
log.Printf("inbound form: %v", err)
|
||||
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?err=inbound", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
if err := db.CreateInbound(s.DB, in); err != nil {
|
||||
log.Printf("create inbound: %v", err)
|
||||
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?err=inbound", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
// Enable on all nodes already using this profile.
|
||||
nodes, _ := db.ListNodes(s.DB)
|
||||
for _, n := range nodes {
|
||||
if n.ProfileID != nil && *n.ProfileID == profileID {
|
||||
@@ -180,6 +250,99 @@ func (s *Server) inboundCreate(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?ok=inbound_added", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) inboundEditGet(w http.ResponseWriter, r *http.Request) {
|
||||
profileID, err := uuid.Parse(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
inboundID, err := uuid.Parse(mux.Vars(r)["inboundID"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid inbound", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
in, err := db.GetInbound(s.DB, inboundID)
|
||||
if err != nil || in == nil || in.ProfileID != profileID {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
p, err := db.GetProfile(s.DB, profileID)
|
||||
if err != nil || p == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
protocols, _ := db.ListProtocols(s.DB)
|
||||
s.render(w, "inbound_edit.html", pageData{
|
||||
"Title": "Inbound " + in.Tag,
|
||||
"UserName": s.Auth.CurrentName(r),
|
||||
"Active": "profiles",
|
||||
"Profile": p,
|
||||
"Inbound": in,
|
||||
"Protocols": protocols,
|
||||
"Flash": r.URL.Query().Get("ok"),
|
||||
"Error": r.URL.Query().Get("err"),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) inboundUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
profileID, err := uuid.Parse(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
inboundID, err := uuid.Parse(mux.Vars(r)["inboundID"])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid inbound", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
existing, err := db.GetInbound(s.DB, inboundID)
|
||||
if err != nil || existing == nil || existing.ProfileID != profileID {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
_ = r.ParseForm()
|
||||
protocolID, err := uuid.Parse(r.FormValue("protocol_id"))
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?err=protocol", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
proto, err := db.GetProtocol(s.DB, protocolID)
|
||||
if err != nil || proto == nil {
|
||||
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?err=protocol", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
in, err := parseInboundForm(r, profileID, protocolID, proto)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?err=keys", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
in.ID = inboundID
|
||||
in.Enabled = false
|
||||
for _, v := range r.Form["enabled"] {
|
||||
if v == "on" || v == "true" {
|
||||
in.Enabled = true
|
||||
}
|
||||
}
|
||||
if in.Security == "reality" {
|
||||
if strings.TrimSpace(r.FormValue("reality_private_key")) == "" {
|
||||
in.RealityPrivateKey = existing.RealityPrivateKey
|
||||
}
|
||||
if strings.TrimSpace(r.FormValue("reality_public_key")) == "" {
|
||||
in.RealityPublicKey = existing.RealityPublicKey
|
||||
}
|
||||
if strings.TrimSpace(r.FormValue("reality_short_id")) == "" && existing.RealityShortID != "" {
|
||||
in.RealityShortID = existing.RealityShortID
|
||||
}
|
||||
}
|
||||
if err := db.UpdateInbound(s.DB, in); err != nil {
|
||||
log.Printf("update inbound: %v", err)
|
||||
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?err=save", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
s.syncProfileNodes(profileID)
|
||||
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"/inbounds/"+inboundID.String()+"?ok=saved", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (s *Server) inboundDelete(w http.ResponseWriter, r *http.Request) {
|
||||
profileID, err := uuid.Parse(mux.Vars(r)["id"])
|
||||
if err != nil {
|
||||
@@ -192,16 +355,7 @@ func (s *Server) inboundDelete(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
_ = db.DeleteInbound(s.DB, inboundID)
|
||||
nodes, _ := db.ListNodes(s.DB)
|
||||
for _, n := range nodes {
|
||||
if n.ProfileID != nil && *n.ProfileID == profileID {
|
||||
_ = db.ApplyProfileInboundsToNodeProtocols(s.DB, n.ID)
|
||||
if n.Status == models.NodeStatusOnline {
|
||||
nn := n
|
||||
_ = s.syncNodeConfig(&nn)
|
||||
}
|
||||
}
|
||||
}
|
||||
s.syncProfileNodes(profileID)
|
||||
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?ok=inbound_deleted", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -217,6 +371,7 @@ func (s *Server) inboundToggle(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
_ = db.ToggleInbound(s.DB, inboundID)
|
||||
s.syncProfileNodes(profileID)
|
||||
http.Redirect(w, r, "/admin/profiles/"+profileID.String()+"?ok=inbound_toggled", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user