A panel without VPN (e.g. after the move) can use a Prometheus that lives on a node, such as the one with history left on the old server (node #0). Both directions go through the agent's WSS: the node's Prometheus scrapes 127.0.0.1:AGENT_METRICS_PORT/metrics, the agent checks its token and asks core (metrics → metrics_res, the same exposition as /metrics, at most once per 5 s per connection); the monitor tab reads it back through the agent, which forwards only GET /-/ready and /api/v1/query[_range]. The panel finds it by itself (local first, then the lowest-id node), and the install/remove buttons work on a panel without VPN. `monitor install` on a node without its own panel picks AGENT_METRICS_PORT, writes it to cli.env and restarts the agent; on the old server it keeps the collected data. The panel works behind nginx in a subfolder: relative asset and API paths, relative directory redirects, and the client address from X-Forwarded-For / X-Real-IP only from trusted proxies (loopback, TRUST_PROXY). migrate import opens the moved SUB_PORT/NODE_PORT in UFW when it is active.
89 lines
2.7 KiB
Go
89 lines
2.7 KiB
Go
package ui
|
|
|
|
// Панель за reverse-proxy (nginx и т. п., в корне домена или в подпапке): соединение приходит от
|
|
// прокси, а адрес клиента — в X-Forwarded-For / X-Real-IP. Иначе все клиенты были бы одним IP и
|
|
// делили бы лимиты по адресу (неудачные входы, регистрации). Заголовкам верим только от прокси:
|
|
// loopback (прокси на этой же машине) всегда, остальные адреса и сети — из TRUST_PROXY. От всех
|
|
// прочих заголовки игнорируются — подделать свой адрес клиенту нечем.
|
|
|
|
import (
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// trustedProxies — TRUST_PROXY: IP и сети (CIDR) через запятую; loopback доверен без него.
|
|
var trustedProxies = sync.OnceValue(func() []*net.IPNet { return parseTrustProxy(os.Getenv("TRUST_PROXY")) })
|
|
|
|
func parseTrustProxy(v string) []*net.IPNet {
|
|
var out []*net.IPNet
|
|
for _, s := range strings.Split(v, ",") {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
continue
|
|
}
|
|
if !strings.Contains(s, "/") {
|
|
if ip := net.ParseIP(s); ip != nil {
|
|
bits := 128
|
|
if ip.To4() != nil {
|
|
bits = 32
|
|
}
|
|
s = ip.String() + "/" + strconv.Itoa(bits)
|
|
}
|
|
}
|
|
if _, n, err := net.ParseCIDR(s); err == nil {
|
|
out = append(out, n)
|
|
} else {
|
|
warnf("TRUST_PROXY: «%s» — не IP и не сеть, пропущено", s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func isTrustedProxy(ip net.IP, nets []*net.IPNet) bool {
|
|
if ip.IsLoopback() {
|
|
return true
|
|
}
|
|
for _, n := range nets {
|
|
if n.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// clientIP — адрес клиента: от доверенного прокси — из X-Forwarded-For (справа налево, первый
|
|
// адрес не из доверенных: левые части клиент мог дописать сам) или X-Real-IP; иначе — сам peer.
|
|
func clientIP(peer string, h interface{ Values(string) []string }, nets []*net.IPNet) string {
|
|
ip := net.ParseIP(peer)
|
|
if ip == nil || !isTrustedProxy(ip, nets) {
|
|
return peer
|
|
}
|
|
var hops []string
|
|
for _, v := range h.Values("X-Forwarded-For") {
|
|
hops = append(hops, strings.Split(v, ",")...)
|
|
}
|
|
found := ""
|
|
for i := len(hops) - 1; i >= 0; i-- {
|
|
hop := net.ParseIP(strings.TrimSpace(hops[i]))
|
|
if hop == nil {
|
|
break
|
|
}
|
|
found = hop.String()
|
|
if !isTrustedProxy(hop, nets) {
|
|
return found
|
|
}
|
|
}
|
|
if found != "" {
|
|
return found
|
|
}
|
|
for _, v := range h.Values("X-Real-IP") {
|
|
if r := net.ParseIP(strings.TrimSpace(v)); r != nil {
|
|
return r.String()
|
|
}
|
|
}
|
|
return peer
|
|
}
|