chore: publish pxmon v0.2.0

This commit is contained in:
2026-06-16 21:52:10 +04:00
commit 6b703db02b
69 changed files with 25886 additions and 0 deletions
+413
View File
@@ -0,0 +1,413 @@
package cluster
import (
"context"
"errors"
"fmt"
"net"
"strconv"
"strings"
)
type RepoTunnelOptions struct {
Gateway string
GatewayIP string
Table int
Priority int
PackageManager string
Command string
KeepEnabled bool
NoRule bool
}
type RepoTunnelState struct {
Enabled bool `json:"enabled"`
Proxy string `json:"proxy,omitempty"`
Source string `json:"source,omitempty"`
}
func (s *Service) RepoTunnelEnable(ctx context.Context, selector string, opts RepoTunnelOptions) (string, error) {
script, err := repoTunnelEnableScript(opts)
if err != nil {
return "", err
}
out, err := s.RunRemoteShell(ctx, selector, script)
if err != nil {
return "", err
}
gw, err := parseRepoTunnelGateway(opts.Gateway)
if err == nil {
_ = s.updateRepoTunnelState(selector, RepoTunnelState{
Enabled: true,
Proxy: gw.proxyURL,
Source: strings.ToLower(strings.TrimSpace(opts.PackageManager)),
})
}
return out, nil
}
func (s *Service) RepoTunnelDisable(ctx context.Context, selector string, opts RepoTunnelOptions) (string, error) {
script, err := repoTunnelDisableScript(opts)
if err != nil {
return "", err
}
out, err := s.RunRemoteShell(ctx, selector, script)
if err != nil {
return "", err
}
_ = s.updateRepoTunnelState(selector, RepoTunnelState{})
return out, nil
}
func (s *Service) RepoTunnelState(ctx context.Context, selector string) (RepoTunnelState, error) {
out, err := s.RunRemoteShell(ctx, selector, repoTunnelDetectScript())
if err != nil {
return RepoTunnelState{}, err
}
state := RepoTunnelState{}
for _, line := range strings.Split(out, "\n") {
line = strings.TrimSpace(line)
switch {
case strings.HasPrefix(line, "proxy="):
state.Proxy = strings.TrimSpace(strings.TrimPrefix(line, "proxy="))
case strings.HasPrefix(line, "source="):
state.Source = strings.TrimSpace(strings.TrimPrefix(line, "source="))
}
}
state.Enabled = state.Proxy != ""
return state, nil
}
func (s *Service) updateRepoTunnelState(selector string, state RepoTunnelState) error {
reg, err := s.store.Load()
if err != nil {
return err
}
c, idx, err := findCluster(reg, selector)
if err != nil {
return err
}
c.RepoTunnel = state
c.UpdatedAt = s.now().UTC()
reg.Clusters[idx] = c
return s.store.Save(reg)
}
func (s *Service) RepoTunnelStatus(ctx context.Context, selector string) (string, error) {
return s.RunRemoteShell(ctx, selector, repoTunnelStatusScript())
}
func (s *Service) RepoTunnelInstall(ctx context.Context, selector string, opts RepoTunnelOptions) (string, error) {
if strings.TrimSpace(opts.Command) == "" {
return "", errors.New("install command is required")
}
enableScript, err := repoTunnelEnableScript(opts)
if err != nil {
return "", err
}
disableScript, err := repoTunnelDisableScript(opts)
if err != nil {
return "", err
}
body := enableScript + "\n" + strings.TrimSpace(opts.Command) + "\n"
if !opts.KeepEnabled {
body = enableScript + "\ncleanup_pxmon_repo_tunnel() {\n" + disableScript + "\n}\ntrap cleanup_pxmon_repo_tunnel EXIT\n" + strings.TrimSpace(opts.Command) + "\n"
}
return s.RunRemoteShell(ctx, selector, body)
}
func RepoTunnelGatewayScript(port int, allowCIDRs []string) (string, error) {
if port == 0 {
port = 3128
}
if port < 1 || port > 65535 {
return "", errors.New("--port must be in range 1..65535")
}
if len(allowCIDRs) == 0 {
return "", errors.New("at least one --allow CIDR/IP is required")
}
aclParts := make([]string, 0, len(allowCIDRs))
for _, raw := range allowCIDRs {
v := strings.TrimSpace(raw)
if v == "" {
continue
}
if !validSquidSrcACL(v) {
return "", fmt.Errorf("invalid --allow %q; use an IP or CIDR without spaces", raw)
}
aclParts = append(aclParts, v)
}
if len(aclParts) == 0 {
return "", errors.New("at least one --allow CIDR/IP is required")
}
return fmt.Sprintf(`set -eu
if command -v dnf >/dev/null 2>&1; then
dnf install -y squid
elif command -v yum >/dev/null 2>&1; then
yum install -y squid
elif command -v apt-get >/dev/null 2>&1; then
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y squid
else
echo "no supported package manager found for squid install" >&2
exit 1
fi
conf=/etc/squid/squid.conf
cp -a "$conf" "$conf.pxmon-bak.$(date +%%Y%%m%%d%%H%%M%%S)"
awk '
/# BEGIN PXMON REPO TUNNEL/ {skip=1; next}
/# END PXMON REPO TUNNEL/ {skip=0; next}
!skip {print}
' "$conf" > "$conf.tmp"
mv "$conf.tmp" "$conf"
block=$(mktemp)
{
echo "# BEGIN PXMON REPO TUNNEL"
if ! grep -Eq "^http_port[[:space:]]+([^[:space:]]+:)?%d\b" "$conf"; then
echo "http_port %d"
fi
echo "acl pxmon_repo_tunnel src %s"
echo "http_access allow pxmon_repo_tunnel"
echo "# END PXMON REPO TUNNEL"
} > "$block"
if grep -q "^http_access deny all" "$conf"; then
awk -v block="$block" '
BEGIN {while ((getline line < block) > 0) b = b line "\n"; close(block); inserted=0}
/^http_access deny all/ && !inserted {printf "%%s", b; inserted=1}
{print}
END {if (!inserted) printf "%%s", b}
' "$conf" > "$conf.tmp"
mv "$conf.tmp" "$conf"
else
cat "$block" >> "$conf"
fi
rm -f "$block"
systemctl enable --now squid
systemctl restart squid
if command -v firewall-cmd >/dev/null 2>&1 && systemctl is-active --quiet firewalld; then
firewall-cmd --add-port=%d/tcp --permanent
firewall-cmd --reload
fi
echo "pxmon repo gateway ready on port %d"
`, port, port, strings.Join(aclParts, " "), port, port), nil
}
func validSquidSrcACL(v string) bool {
for _, r := range v {
if r >= 'a' && r <= 'z' {
continue
}
if r >= 'A' && r <= 'Z' {
continue
}
if r >= '0' && r <= '9' {
continue
}
switch r {
case '.', ':', '/', '_', '-':
continue
default:
return false
}
}
return v != ""
}
func repoTunnelEnableScript(opts RepoTunnelOptions) (string, error) {
gw, err := parseRepoTunnelGateway(opts.Gateway)
if err != nil {
return "", err
}
if !opts.NoRule && opts.Table <= 0 {
return "", errors.New("--table is required unless --no-rule is used")
}
manager := strings.ToLower(strings.TrimSpace(opts.PackageManager))
if manager == "" {
manager = "auto"
}
if manager != "auto" && manager != "apt" && manager != "dnf" && manager != "yum" {
return "", fmt.Errorf("unsupported --manager %q", opts.PackageManager)
}
ruleLine := ""
if !opts.NoRule {
addCmd := fmt.Sprintf("ip rule add to \"$PXMON_GATEWAY_IP/32\" table %d", opts.Table)
if opts.Priority > 0 {
addCmd = fmt.Sprintf("ip rule add priority %d to \"$PXMON_GATEWAY_IP/32\" table %d", opts.Priority, opts.Table)
}
ruleLine = fmt.Sprintf(`
if ! ip rule show | grep -Eq "to[[:space:]]+$PXMON_GATEWAY_IP(/32)?[[:space:]].*lookup[[:space:]]+%d\b"; then
%s
fi`, opts.Table, addCmd)
}
return fmt.Sprintf(`set -eu
PXMON_GATEWAY_HOST=%s
PXMON_GATEWAY_IP=%s
PXMON_PROXY_URL=%s
PXMON_MANAGER=%s
if [ -z "$PXMON_GATEWAY_IP" ]; then
PXMON_GATEWAY_IP=$(getent ahostsv4 "$PXMON_GATEWAY_HOST" | awk '{print $1; exit}')
fi
if [ -z "$PXMON_GATEWAY_IP" ]; then
echo "cannot resolve repo gateway: $PXMON_GATEWAY_HOST" >&2
exit 1
fi
%s
pxmon_repo_manager="$PXMON_MANAGER"
if [ "$pxmon_repo_manager" = "auto" ]; then
if command -v apt-get >/dev/null 2>&1; then pxmon_repo_manager=apt
elif command -v dnf >/dev/null 2>&1; then pxmon_repo_manager=dnf
elif command -v yum >/dev/null 2>&1; then pxmon_repo_manager=yum
else echo "no supported package manager found" >&2; exit 1
fi
fi
case "$pxmon_repo_manager" in
apt)
mkdir -p /etc/apt/apt.conf.d
cat > /etc/apt/apt.conf.d/99-pxmon-repo-tunnel <<EOF
Acquire::http::Proxy "$PXMON_PROXY_URL";
Acquire::https::Proxy "$PXMON_PROXY_URL";
EOF
;;
dnf|yum)
conf=/etc/dnf/dnf.conf
[ "$pxmon_repo_manager" = "yum" ] && conf=/etc/yum.conf
[ -f "$conf" ] || touch "$conf"
awk '
/# BEGIN PXMON REPO TUNNEL/ {skip=1; next}
/# END PXMON REPO TUNNEL/ {skip=0; next}
!skip {print}
' "$conf" > "$conf.tmp"
mv "$conf.tmp" "$conf"
{
echo "# BEGIN PXMON REPO TUNNEL"
echo "proxy=$PXMON_PROXY_URL"
echo "# END PXMON REPO TUNNEL"
} >> "$conf"
;;
esac
echo "pxmon repo tunnel enabled: proxy=$PXMON_PROXY_URL gateway_ip=$PXMON_GATEWAY_IP manager=$pxmon_repo_manager"
`, shellQuote(gw.host), shellQuote(strings.TrimSpace(opts.GatewayIP)), shellQuote(gw.proxyURL), shellQuote(manager), ruleLine), nil
}
func repoTunnelDisableScript(opts RepoTunnelOptions) (string, error) {
gw, err := parseRepoTunnelGateway(opts.Gateway)
if err != nil && !opts.NoRule {
return "", err
}
ruleLine := ""
if !opts.NoRule {
if opts.Table <= 0 {
return "", errors.New("--table is required unless --no-rule is used")
}
ruleLine = fmt.Sprintf(`
PXMON_GATEWAY_HOST=%s
PXMON_GATEWAY_IP=%s
if [ -z "$PXMON_GATEWAY_IP" ]; then
PXMON_GATEWAY_IP=$(getent ahostsv4 "$PXMON_GATEWAY_HOST" | awk '{print $1; exit}')
fi
if [ -n "$PXMON_GATEWAY_IP" ]; then
while ip rule show | grep -Eq "to[[:space:]]+$PXMON_GATEWAY_IP(/32)?[[:space:]].*lookup[[:space:]]+%d\b"; do
ip rule del to "$PXMON_GATEWAY_IP/32" table %d 2>/dev/null || break
done
fi`, shellQuote(gw.host), shellQuote(strings.TrimSpace(opts.GatewayIP)), opts.Table, opts.Table)
}
return fmt.Sprintf(`set -eu
rm -f /etc/apt/apt.conf.d/99-pxmon-repo-tunnel
for conf in /etc/dnf/dnf.conf /etc/yum.conf; do
if [ -f "$conf" ]; then
awk '
/# BEGIN PXMON REPO TUNNEL/ {skip=1; next}
/# END PXMON REPO TUNNEL/ {skip=0; next}
!skip {print}
' "$conf" > "$conf.tmp"
mv "$conf.tmp" "$conf"
fi
done
%s
echo "pxmon repo tunnel disabled"
`, ruleLine), nil
}
func repoTunnelStatusScript() string {
return `set -eu
echo "== ip rules =="
ip rule show | grep -E "lookup|table" || true
echo
echo "== apt proxy =="
[ -f /etc/apt/apt.conf.d/99-pxmon-repo-tunnel ] && cat /etc/apt/apt.conf.d/99-pxmon-repo-tunnel || echo "(none)"
echo
echo "== dnf/yum proxy =="
for conf in /etc/dnf/dnf.conf /etc/yum.conf; do
[ -f "$conf" ] || continue
echo "-- $conf"
awk '/# BEGIN PXMON REPO TUNNEL/,/# END PXMON REPO TUNNEL/ {print}' "$conf"
done`
}
func repoTunnelDetectScript() string {
return `set -eu
if [ -f /etc/apt/apt.conf.d/99-pxmon-repo-tunnel ]; then
proxy=$(sed -n 's/.*Proxy[[:space:]]*"\([^"]*\)".*/\1/p' /etc/apt/apt.conf.d/99-pxmon-repo-tunnel | head -1)
[ -n "$proxy" ] && printf 'proxy=%s\nsource=apt\n' "$proxy" && exit 0
fi
for conf in /etc/dnf/dnf.conf /etc/yum.conf; do
[ -f "$conf" ] || continue
proxy=$(awk '
/# BEGIN PXMON REPO TUNNEL/ {inside=1; next}
/# END PXMON REPO TUNNEL/ {inside=0; next}
inside && /^proxy[[:space:]]*=/ {
sub(/^[^=]*=/, "")
gsub(/^[[:space:]]+|[[:space:]]+$/, "")
print
exit
}
' "$conf")
[ -n "$proxy" ] && printf 'proxy=%s\nsource=%s\n' "$proxy" "$conf" && exit 0
done
exit 0`
}
type repoTunnelGateway struct {
host string
port int
proxyURL string
}
func parseRepoTunnelGateway(raw string) (repoTunnelGateway, error) {
v := strings.TrimSpace(raw)
if v == "" {
return repoTunnelGateway{}, errors.New("--gateway is required")
}
if strings.HasPrefix(v, "http://") {
v = strings.TrimPrefix(v, "http://")
}
if strings.HasPrefix(v, "https://") {
return repoTunnelGateway{}, errors.New("--gateway must be an http proxy endpoint, not https")
}
host, portRaw, err := net.SplitHostPort(v)
if err != nil {
if strings.Count(v, ":") > 1 {
return repoTunnelGateway{}, fmt.Errorf("invalid --gateway %q; use host:port or [ipv6]:port", raw)
}
host = v
portRaw = "3128"
}
host = strings.Trim(host, "[]")
if strings.TrimSpace(host) == "" {
return repoTunnelGateway{}, errors.New("--gateway host is empty")
}
port, err := strconv.Atoi(portRaw)
if err != nil || port < 1 || port > 65535 {
return repoTunnelGateway{}, fmt.Errorf("invalid --gateway port %q", portRaw)
}
return repoTunnelGateway{
host: host,
port: port,
proxyURL: "http://" + net.JoinHostPort(host, strconv.Itoa(port)),
}, nil
}