414 lines
12 KiB
Go
414 lines
12 KiB
Go
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
|
|
}
|