From 22e13c9c0d1624e4e36133914c6465f7d3dba4e9 Mon Sep 17 00:00:00 2001 From: loveuer Date: Wed, 4 Mar 2026 23:36:09 -0800 Subject: [PATCH] feat: add GitHub Actions release workflow and improve docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GitHub Actions workflow for multi-platform releases - Build for linux/darwin on amd64/arm64 - Auto-create GitHub Release with checksums - Version injection via ldflags - Add init.d script support for install command - Rewrite README with clearer documentation - Quick start guide - Two-node HA setup example - Health check configuration - Troubleshooting section - Bump version to 1.2.1 🤖 Generated with [Qoder][https://qoder.com] --- .github/workflows/release.yml | 95 +++++++++ README.md | 373 +++++++++++++++++++--------------- internal/cmd/install.go | 161 +++++++++++++-- internal/cmd/root.go | 5 +- 4 files changed, 451 insertions(+), 183 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..72dfbfd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,95 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + build: + name: Build ${{ matrix.os }}-${{ matrix.arch }} + runs-on: ubuntu-latest + strategy: + matrix: + include: + - os: linux + arch: amd64 + - os: linux + arch: arm64 + - os: darwin + arch: amd64 + - os: darwin + arch: arm64 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.21' + + - name: Get version + id: version + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: Build binary + env: + GOOS: ${{ matrix.os }} + GOARCH: ${{ matrix.arch }} + CGO_ENABLED: 0 + run: | + mkdir -p dist + BINARY_NAME=go-alived-${{ matrix.os }}-${{ matrix.arch }} + if [ "${{ matrix.os }}" = "windows" ]; then + BINARY_NAME="${BINARY_NAME}.exe" + fi + go build -ldflags="-s -w -X github.com/loveuer/go-alived/internal/cmd.Version=${{ steps.version.outputs.VERSION }}" \ + -o dist/${BINARY_NAME} . + + # Create checksum + cd dist && sha256sum ${BINARY_NAME} > ${BINARY_NAME}.sha256 + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: go-alived-${{ matrix.os }}-${{ matrix.arch }} + path: dist/ + + release: + name: Create Release + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Prepare release files + run: | + mkdir -p release + find artifacts -type f -exec cp {} release/ \; + ls -la release/ + + - name: Get version + id: version + run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + name: Release ${{ steps.version.outputs.VERSION }} + draft: false + prerelease: ${{ contains(github.ref, '-rc') || contains(github.ref, '-beta') || contains(github.ref, '-alpha') }} + generate_release_notes: true + files: release/* + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index db02427..1484bb1 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,35 @@ # go-alived -A lightweight, dependency-free VRRP (Virtual Router Redundancy Protocol) implementation in Go, designed as a simple alternative to keepalived. +A lightweight VRRP (Virtual Router Redundancy Protocol) implementation in Go, designed as a simple alternative to keepalived. ## Features -✅ **Phase 1: Core VRRP Functionality (Completed)** -- VRRP protocol implementation (RFC 3768/5798) -- Virtual IP management (add/remove VIPs) -- State machine (INIT/BACKUP/MASTER/FAULT) -- Priority-based master election -- Gratuitous ARP for network updates -- Raw socket VRRP packet send/receive -- Timer management (advertisement & master-down timers) -- VRRP instance manager with multi-instance support -- Configuration hot-reload (SIGHUP) - -✅ **Phase 2: Health Checking (Completed)** -- Health checker interface with rise/fall logic -- TCP health checks -- HTTP/HTTPS health checks -- ICMP ping checks -- Script-based checks (custom commands) -- Periodic health check scheduling -- Health check integration with VRRP priority -- Track scripts: automatic priority adjustment on health changes - -🚧 **Phase 3: Enhanced Features (Planned)** -- State transition scripts (notify_master/backup/fault) -- Email/Webhook notifications -- Sync groups -- Virtual MAC support -- Metrics export +- **VRRP Protocol**: RFC 3768/5798 compliant implementation +- **High Availability**: Automatic failover with priority-based master election +- **Health Checking**: TCP, HTTP/HTTPS, ICMP ping, and script-based checks +- **Easy Deployment**: Built-in install command with systemd/init.d support +- **Hot Reload**: Configuration reload via SIGHUP without service restart +- **Zero Dependencies**: Single static binary, no runtime dependencies ## Installation -### Build from source +### Download Binary + +Download the latest release from [GitHub Releases](https://github.com/loveuer/go-alived/releases): + +```bash +# Linux amd64 +curl -LO https://github.com/loveuer/go-alived/releases/latest/download/go-alived-linux-amd64 +chmod +x go-alived-linux-amd64 +sudo mv go-alived-linux-amd64 /usr/local/bin/go-alived + +# Linux arm64 +curl -LO https://github.com/loveuer/go-alived/releases/latest/download/go-alived-linux-arm64 +chmod +x go-alived-linux-arm64 +sudo mv go-alived-linux-arm64 /usr/local/bin/go-alived +``` + +### Build from Source ```bash git clone https://github.com/loveuer/go-alived.git @@ -42,190 +37,246 @@ cd go-alived go build -o go-alived . ``` +### Quick Install (Recommended) + +```bash +# Install as systemd service (default) +sudo ./go-alived install + +# Install as init.d service (for OpenWrt/older systems) +sudo ./go-alived install --method service +``` + ## Quick Start -### 1. Test Your Environment - -Before deployment, test if your environment supports VRRP: +### 1. Test Environment ```bash -# Basic test (auto-detect network interface) -sudo ./go-alived test +# Check if your environment supports VRRP +sudo go-alived test -# Test specific interface -sudo ./go-alived test -i eth0 - -# Full test with VIP -sudo ./go-alived test -i eth0 -v 192.168.1.100/24 +# Test with specific interface +sudo go-alived test -i eth0 ``` -### 2. Run the Service +### 2. Configure + +Edit `/etc/go-alived/config.yaml`: + +```yaml +global: + router_id: "node1" + +vrrp_instances: + - name: "VI_1" + interface: "eth0" # Network interface + state: "BACKUP" # Initial state + virtual_router_id: 51 # VRID (1-255, must match on all nodes) + priority: 100 # Higher = more likely to be master + advert_interval: 1 # Advertisement interval in seconds + auth_type: "PASS" # Authentication type + auth_pass: "secret" # Password (max 8 chars) + virtual_ips: + - "192.168.1.100/24" # Virtual IP address(es) +``` + +### 3. Start Service ```bash -# Run with minimal config -sudo ./go-alived run -c config.mini.yaml -d - -# Run with full config -sudo ./go-alived -c config.yaml - -# Install as systemd service -sudo ./deployment/install.sh +# Systemd +sudo systemctl daemon-reload +sudo systemctl enable go-alived sudo systemctl start go-alived + +# Init.d +sudo /etc/init.d/go-alived start ``` -## Usage +### 4. Verify -### Commands +```bash +# Check service status +sudo systemctl status go-alived +# Check VIP +ip addr show eth0 | grep 192.168.1.100 + +# View logs +sudo journalctl -u go-alived -f ``` -go-alived # Run VRRP service (default) -go-alived run # Run VRRP service -go-alived test # Test environment for VRRP support -go-alived --help # Show help -go-alived --version # Show version -``` - -### Global Flags - -``` --c, --config string Path to configuration file (default "/etc/go-alived/config.yaml") --d, --debug Enable debug mode --h, --help Show help --v, --version Show version -``` - -### Test Command Flags - -``` --i, --interface string Network interface to test (auto-detect if not specified) --v, --vip string Test VIP address (e.g., 192.168.1.100/24) -``` - -See [USAGE.md](USAGE.md) for detailed usage documentation. ## Configuration -### Minimal Configuration +### Two-Node HA Setup Example +**Node 1 (Primary)**: ```yaml -# config.mini.yaml - VRRP only global: router_id: "node1" vrrp_instances: - name: "VI_1" interface: "eth0" - state: "BACKUP" + state: "MASTER" virtual_router_id: 51 - priority: 100 + priority: 100 # Higher priority advert_interval: 1 auth_type: "PASS" - auth_pass: "secret123" + auth_pass: "secret" virtual_ips: - "192.168.1.100/24" ``` -### Full Configuration Example +**Node 2 (Backup)**: +```yaml +global: + router_id: "node2" -See `config.example.yaml` for complete configuration with health checking. +vrrp_instances: + - name: "VI_1" + interface: "eth0" + state: "BACKUP" + virtual_router_id: 51 + priority: 90 # Lower priority + advert_interval: 1 + auth_type: "PASS" + auth_pass: "secret" # Must match + virtual_ips: + - "192.168.1.100/24" # Must match +``` -### Signals +### Health Checking -- `SIGHUP`: Reload configuration -- `SIGINT/SIGTERM`: Graceful shutdown +```yaml +vrrp_instances: + - name: "VI_1" + # ... other settings ... + track_scripts: + - "check_nginx" # Reference to health checker -## Architecture +health_checkers: + - name: "check_nginx" + type: "tcp" + interval: 3s + timeout: 2s + rise: 3 # Successes to mark healthy + fall: 2 # Failures to mark unhealthy + config: + host: "127.0.0.1" + port: 80 +``` + +**Supported Health Check Types**: + +| Type | Description | Config | +|------|-------------|--------| +| `tcp` | TCP port check | `host`, `port` | +| `http` | HTTP endpoint check | `url`, `method`, `expected_status` | +| `ping` | ICMP ping check | `host`, `count` | +| `script` | Custom script | `script`, `args` | + +## Commands ``` -go-alived/ -├── main.go # Application entry point -├── internal/ -│ ├── cmd/ # Cobra commands -│ │ ├── root.go # Root command -│ │ ├── run.go # Run service command -│ │ └── test.go # Environment test command -│ ├── vrrp/ # VRRP implementation -│ │ ├── packet.go # VRRP packet structure & marshaling -│ │ ├── socket.go # Raw socket operations -│ │ ├── state.go # State machine & timers -│ │ ├── arp.go # Gratuitous ARP -│ │ ├── instance.go # VRRP instance logic -│ │ └── manager.go # Instance manager -│ └── health/ # Health check system -│ ├── checker.go # Checker interface & state -│ ├── monitor.go # Health check scheduler -│ ├── tcp.go # TCP health checker -│ ├── http.go # HTTP/HTTPS health checker -│ ├── ping.go # ICMP ping checker -│ ├── script.go # Script checker -│ └── factory.go # Checker factory -├── pkg/ -│ ├── config/ # Configuration loading & validation -│ ├── logger/ # Logging system -│ └── netif/ # Network interface management -└── deployment/ # Deployment files - ├── go-alived.service # Systemd service file - ├── install.sh # Installation script - ├── uninstall.sh # Uninstallation script - ├── check-env.sh # Environment check script - ├── README.md # Deployment documentation - └── COMPATIBILITY.md # Environment compatibility guide +go-alived [command] + +Available Commands: + run Run the VRRP service + test Test environment for VRRP support + install Install go-alived as a system service (alias: i) + help Help about any command + +Flags: + -h, --help help for go-alived + -v, --version version for go-alived +``` + +### run + +```bash +go-alived run [flags] + +Flags: + -c, --config string Path to config file (default "/etc/go-alived/config.yaml") + -d, --debug Enable debug mode +``` + +### test + +```bash +go-alived test [flags] + +Flags: + -i, --interface string Network interface to test + -v, --vip string Test VIP address (e.g., 192.168.1.100/24) +``` + +### install + +```bash +go-alived install [flags] + +Flags: + -m, --method string Installation method: systemd, service (default "systemd") + +Aliases: + install, i +``` + +## Signals + +| Signal | Action | +|--------|--------| +| `SIGHUP` | Reload configuration | +| `SIGINT` / `SIGTERM` | Graceful shutdown | + +```bash +# Reload configuration +sudo kill -HUP $(pgrep go-alived) ``` ## Environment Compatibility -### ✅ Fully Supported -- Physical servers -- KVM/QEMU virtual machines -- Proxmox VE -- VMware ESXi (with promiscuous mode) -- VirtualBox (with bridged network + promiscuous mode) +| Environment | Support | Notes | +|-------------|---------|-------| +| Physical servers | Full | | +| KVM/QEMU/Proxmox | Full | | +| VMware ESXi | Full | Enable promiscuous mode | +| VirtualBox | Full | Bridged network + promiscuous mode | +| Docker | Limited | Requires `--privileged --net=host` | +| OpenWrt/iStoreOS | Full | Use `--method service` for install | +| AWS/Aliyun/Azure | None | Multicast disabled | -### ⚠️ Limited Support -- Private cloud (depends on network configuration) -- Docker containers (requires `--privileged` and `--net=host`) -- Kubernetes (requires hostNetwork mode) +> **Note**: VRRP requires multicast support (224.0.0.18). Most public clouds disable multicast at the network layer. Use cloud-native HA solutions instead. -### ❌ Not Supported -- AWS EC2 (multicast disabled) -- Aliyun ECS (multicast disabled) -- Azure VM (requires special configuration) -- Google Cloud (multicast disabled by default) +## Troubleshooting -**Why?** Public clouds typically disable multicast protocols (224.0.0.18) at the network virtualization layer. +### Common Issues -**Alternative**: Use cloud-native solutions like Elastic IP (AWS), SLB/HaVip (Aliyun), Load Balancer (Azure/GCP). +**1. "permission denied" or "operation not permitted"** +```bash +# VRRP requires root privileges +sudo go-alived run -c /etc/go-alived/config.yaml +``` -See [deployment/COMPATIBILITY.md](deployment/COMPATIBILITY.md) for detailed compatibility information. +**2. "authentication failed"** +- Ensure `auth_pass` matches on all nodes +- Password is limited to 8 characters -## Requirements +**3. Both nodes become MASTER (split-brain)** +- Check network connectivity between nodes +- Verify `virtual_router_id` matches +- Ensure multicast traffic is allowed -- Go 1.21+ (for building) -- Linux/macOS with root privileges (for raw sockets and interface management) -- Network interface with IPv4 address -- Multicast support (for VRRP) +**4. VIP not pingable after failover** +- Gratuitous ARP may be blocked +- Check switch/router ARP cache timeout -## Dependencies +### Debug Mode -Minimal external dependencies: -- `github.com/vishvananda/netlink` - Network interface management -- `github.com/mdlayher/arp` - ARP packet handling -- `github.com/spf13/cobra` - CLI framework -- `golang.org/x/net/ipv4` - IPv4 raw socket support -- `golang.org/x/net/icmp` - ICMP ping support -- `gopkg.in/yaml.v3` - YAML configuration parsing - -## Documentation - -- [USAGE.md](USAGE.md) - Detailed usage guide -- [TESTING.md](TESTING.md) - Testing guide -- [deployment/README.md](deployment/README.md) - Deployment guide -- [deployment/COMPATIBILITY.md](deployment/COMPATIBILITY.md) - Environment compatibility -- [roadmap.md](roadmap.md) - Implementation roadmap - -## Roadmap - -See [roadmap.md](roadmap.md) for detailed implementation plan. +```bash +sudo go-alived run -c /etc/go-alived/config.yaml -d +``` ## License diff --git a/internal/cmd/install.go b/internal/cmd/install.go index 0f421dc..cf5027d 100644 --- a/internal/cmd/install.go +++ b/internal/cmd/install.go @@ -16,6 +16,7 @@ const ( defaultConfigDir = "/etc/go-alived" defaultConfigFile = "/etc/go-alived/config.yaml" systemdServicePath = "/etc/systemd/system/go-alived.service" + initdScriptPath = "/etc/init.d/go-alived" ) var ( @@ -30,7 +31,7 @@ var installCmd = &cobra.Command{ Supported installation methods: - systemd: Install as a systemd service (default, recommended for modern Linux) - - service: Install binary and config only (manual startup) + - service: Install as a SysV init.d service (for older Linux distributions) Examples: sudo go-alived install @@ -65,10 +66,7 @@ func runInstall(cmd *cobra.Command, args []string) { fmt.Println("=== Go-Alived Installation ===") fmt.Println() - totalSteps := 2 - if method == "systemd" { - totalSteps = 3 - } + const totalSteps = 3 // Step 1: Copy binary if err := installBinary(1, totalSteps); err != nil { @@ -83,12 +81,10 @@ func runInstall(cmd *cobra.Command, args []string) { os.Exit(1) } - // Step 3: Install systemd service if requested - if method == "systemd" { - if err := installSystemdService(3, totalSteps); err != nil { - fmt.Printf("Error installing systemd service: %v\n", err) - os.Exit(1) - } + // Step 3: Install service script + if err := installServiceScript(3, totalSteps, method); err != nil { + fmt.Printf("Error installing service script: %v\n", err) + os.Exit(1) } // Print completion message @@ -165,6 +161,17 @@ func installConfig(step, total int) (bool, error) { return true, nil } +func installServiceScript(step, total int, method string) error { + switch method { + case "systemd": + return installSystemdService(step, total) + case "service": + return installInitdScript(step, total) + default: + return fmt.Errorf("unsupported method: %s", method) + } +} + func installSystemdService(step, total int) error { fmt.Printf("[%d/%d] Installing systemd service... ", step, total) @@ -178,6 +185,19 @@ func installSystemdService(step, total int) error { return nil } +func installInitdScript(step, total int) error { + fmt.Printf("[%d/%d] Installing init.d script... ", step, total) + + scriptContent := generateInitdScript() + + if err := os.WriteFile(initdScriptPath, []byte(scriptContent), 0755); err != nil { + return fmt.Errorf("failed to write init.d script: %w", err) + } + + fmt.Printf("done (%s)\n", initdScriptPath) + return nil +} + func generateDefaultConfig() string { // Auto-detect network interface iface := detectNetworkInterface() @@ -261,6 +281,90 @@ WantedBy=multi-user.target ` } +func generateInitdScript() string { + return `#!/bin/sh +### BEGIN INIT INFO +# Provides: go-alived +# Required-Start: $network $remote_fs $syslog +# Required-Stop: $network $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Go-Alived VRRP High Availability Service +# Description: Lightweight VRRP implementation for IP high availability +### END INIT INFO + +NAME="go-alived" +DAEMON="/usr/local/bin/go-alived" +DAEMON_ARGS="run --config /etc/go-alived/config.yaml" +PIDFILE="/var/run/${NAME}.pid" +LOGFILE="/var/log/${NAME}.log" + +[ -x "$DAEMON" ] || exit 5 + +start() { + if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + echo "$NAME is already running" + return 1 + fi + echo -n "Starting $NAME... " + nohup $DAEMON $DAEMON_ARGS >> "$LOGFILE" 2>&1 & + echo $! > "$PIDFILE" + echo "done (PID: $(cat "$PIDFILE"))" +} + +stop() { + if [ ! -f "$PIDFILE" ] || ! kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + echo "$NAME is not running" + return 1 + fi + echo -n "Stopping $NAME... " + kill "$(cat "$PIDFILE")" + rm -f "$PIDFILE" + echo "done" +} + +restart() { + stop + sleep 1 + start +} + +reload() { + if [ ! -f "$PIDFILE" ] || ! kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + echo "$NAME is not running" + return 1 + fi + echo -n "Reloading $NAME configuration... " + kill -HUP "$(cat "$PIDFILE")" + echo "done" +} + +status() { + if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + echo "$NAME is running (PID: $(cat "$PIDFILE"))" + else + echo "$NAME is not running" + [ -f "$PIDFILE" ] && rm -f "$PIDFILE" + return 1 + fi +} + +case "$1" in + start) start ;; + stop) stop ;; + restart) restart ;; + reload) reload ;; + status) status ;; + *) + echo "Usage: $0 {start|stop|restart|reload|status}" + exit 2 + ;; +esac + +exit $? +` +} + func detectNetworkInterface() string { interfaces, err := net.Interfaces() if err != nil { @@ -299,18 +403,29 @@ func printCompletionMessage(method string, configCreated bool) { fmt.Println("=== Installation Complete ===") fmt.Println() + // Installed files summary + fmt.Println(">>> Installed Files:") + fmt.Printf(" Binary: %s\n", defaultBinaryPath) + fmt.Printf(" Config: %s\n", defaultConfigFile) + if method == "systemd" { + fmt.Printf(" Service: %s\n", systemdServicePath) + } else { + fmt.Printf(" Service: %s\n", initdScriptPath) + } + fmt.Println() + // What needs to be modified fmt.Println(">>> Configuration Required:") fmt.Printf(" Edit: %s\n", defaultConfigFile) fmt.Println() - fmt.Println(" Modify the following settings:") if configCreated { - fmt.Println(" - auth_pass: Change 'changeme' to a secure password") - fmt.Println(" - virtual_ips: Set your Virtual IP address(es)") - fmt.Println(" - interface: Verify the network interface is correct") - fmt.Println(" - priority: Adjust based on node role (higher = more likely to be master)") + fmt.Println(" Modify the following settings:") + fmt.Println(" - auth_pass: Change 'changeme' to a secure password") + fmt.Println(" - virtual_ips: Set your Virtual IP address(es)") + fmt.Println(" - interface: Verify the network interface is correct") + fmt.Println(" - priority: Adjust based on node role (higher = more likely master)") } else { - fmt.Println(" - Review your existing configuration") + fmt.Println(" Review your existing configuration") } fmt.Println() @@ -332,11 +447,15 @@ func printCompletionMessage(method string, configCreated bool) { fmt.Println(" 1. Edit configuration:") fmt.Printf(" sudo vim %s\n", defaultConfigFile) fmt.Println() - fmt.Println(" 2. Run manually:") - fmt.Printf(" sudo %s run -c %s\n", defaultBinaryPath, defaultConfigFile) + fmt.Println(" 2. Start service:") + fmt.Printf(" sudo %s start\n", initdScriptPath) fmt.Println() - fmt.Println(" 3. Or run in debug mode:") - fmt.Printf(" sudo %s run -c %s -d\n", defaultBinaryPath, defaultConfigFile) + fmt.Println(" 3. Enable on boot (Debian/Ubuntu):") + fmt.Println(" sudo update-rc.d go-alived defaults") + fmt.Println() + fmt.Println(" 4. Check service status:") + fmt.Printf(" sudo %s status\n", initdScriptPath) + fmt.Printf(" tail -f /var/log/go-alived.log\n") } fmt.Println() diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 849bbab..7bb60ae 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -6,12 +6,14 @@ import ( "github.com/spf13/cobra" ) +// Version can be set at build time via ldflags +var Version = "1.2.1" + var rootCmd = &cobra.Command{ Use: "go-alived", Short: "Go-Alived - VRRP High Availability Service", Long: `go-alived is a lightweight, dependency-free VRRP implementation in Go. It provides high availability for IP addresses with health checking support.`, - Version: "1.2.0", } func Execute() { @@ -21,5 +23,6 @@ func Execute() { } func init() { + rootCmd.Version = Version rootCmd.CompletionOptions.DisableDefaultCmd = true } \ No newline at end of file