Files
forgetting/internal/ui/trustproxy.go
T
maeneko 96c21408e6 Prometheus on a node; panel under a URL prefix; open ports on migrate import
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.
2026-10-21 19:25:54 +03:00

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
}