├── .github └── workflows │ └── release.yaml ├── .gitignore ├── .goreleaser.yaml ├── LICENSE.md ├── README.md ├── assets └── images │ └── web.png ├── cmd ├── list.go ├── root.go ├── send.go ├── serve.go ├── templates │ └── index.html └── version.go ├── config.example.yaml ├── config └── config.go ├── docker ├── Dockerfile └── Dockerfile.template ├── examples └── reverse-proxy.yml ├── go.mod ├── go.sum ├── magicpacket └── magicpacket.go └── main.go /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | # run only against tags 6 | tags: 7 | - "*" 8 | 9 | permissions: 10 | contents: write 11 | packages: write # Add permission for publishing packages 12 | 13 | jobs: 14 | goreleaser: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Set up Go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: stable 25 | 26 | - name: Set up QEMU 27 | uses: docker/setup-qemu-action@v3 28 | 29 | - name: Set up Docker Buildx 30 | uses: docker/setup-buildx-action@v3 31 | 32 | - name: Login to GitHub Container Registry 33 | uses: docker/login-action@v3 34 | with: 35 | registry: ghcr.io 36 | username: ${{ github.actor }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Run GoReleaser 40 | uses: goreleaser/goreleaser-action@v6 41 | with: 42 | # either 'goreleaser' (default) or 'goreleaser-pro' 43 | distribution: goreleaser 44 | version: "~> v2" 45 | args: release --clean 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build artifacts 2 | wol 3 | *.exe 4 | dist/ 5 | 6 | # Local config file 7 | config.yaml -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | before: 4 | hooks: 5 | - go mod tidy 6 | - go generate ./... 7 | 8 | builds: 9 | - env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - linux 13 | - windows 14 | - darwin 15 | goarch: 16 | - amd64 17 | - arm64 18 | - arm 19 | - "386" 20 | goarm: 21 | - "7" 22 | ldflags: 23 | - -s -w 24 | - -X github.com/trugamr/wol/cmd.version={{.Version}} 25 | - -X github.com/trugamr/wol/cmd.commit={{.Commit}} 26 | - -X github.com/trugamr/wol/cmd.date={{.Date}} 27 | 28 | archives: 29 | - format: tar.gz 30 | name_template: >- 31 | {{ .ProjectName }}_ 32 | {{- title .Os }}_ 33 | {{- if eq .Arch "amd64" }}x86_64 34 | {{- else if eq .Arch "386" }}i386 35 | {{- else }}{{ .Arch }}{{ end }} 36 | {{- if .Arm }}v{{ .Arm }}{{ end }} 37 | format_overrides: 38 | - goos: windows 39 | format: zip 40 | 41 | dockers: 42 | - image_templates: 43 | - "ghcr.io/trugamr/wol:{{ .Version }}-amd64" 44 | - "ghcr.io/trugamr/wol:latest-amd64" 45 | dockerfile: docker/Dockerfile.template 46 | use: buildx 47 | build_flag_templates: 48 | - "--platform=linux/amd64" 49 | - "--label=org.opencontainers.image.title={{ .ProjectName }}" 50 | - "--label=org.opencontainers.image.description=A Wake-On-LAN tool that works via CLI and web interface" 51 | - "--label=org.opencontainers.image.url=https://github.com/trugamr/wol" 52 | - "--label=org.opencontainers.image.source=https://github.com/trugamr/wol" 53 | - "--label=org.opencontainers.image.version={{ .Version }}" 54 | - "--label=org.opencontainers.image.created={{ .Date }}" 55 | - "--label=org.opencontainers.image.revision={{ .FullCommit }}" 56 | - "--label=org.opencontainers.image.licenses=MIT" 57 | - image_templates: 58 | - "ghcr.io/trugamr/wol:{{ .Version }}-arm64" 59 | - "ghcr.io/trugamr/wol:latest-arm64" 60 | dockerfile: docker/Dockerfile.template 61 | use: buildx 62 | goarch: arm64 63 | build_flag_templates: 64 | - "--platform=linux/arm64" 65 | - "--label=org.opencontainers.image.title={{ .ProjectName }}" 66 | - "--label=org.opencontainers.image.description=A Wake-On-LAN tool that works via CLI and web interface" 67 | - "--label=org.opencontainers.image.url=https://github.com/trugamr/wol" 68 | - "--label=org.opencontainers.image.source=https://github.com/trugamr/wol" 69 | - "--label=org.opencontainers.image.version={{ .Version }}" 70 | - "--label=org.opencontainers.image.created={{ .Date }}" 71 | - "--label=org.opencontainers.image.revision={{ .FullCommit }}" 72 | - "--label=org.opencontainers.image.licenses=MIT" 73 | - image_templates: 74 | - "ghcr.io/trugamr/wol:{{ .Version }}-armv7" 75 | - "ghcr.io/trugamr/wol:latest-armv7" 76 | dockerfile: docker/Dockerfile.template 77 | use: buildx 78 | goarch: arm 79 | goarm: 7 80 | build_flag_templates: 81 | - "--platform=linux/arm/v7" 82 | - "--label=org.opencontainers.image.title={{ .ProjectName }}" 83 | - "--label=org.opencontainers.image.description=A Wake-On-LAN tool that works via CLI and web interface" 84 | - "--label=org.opencontainers.image.url=https://github.com/trugamr/wol" 85 | - "--label=org.opencontainers.image.source=https://github.com/trugamr/wol" 86 | - "--label=org.opencontainers.image.version={{ .Version }}" 87 | - "--label=org.opencontainers.image.created={{ .Date }}" 88 | - "--label=org.opencontainers.image.revision={{ .FullCommit }}" 89 | - "--label=org.opencontainers.image.licenses=MIT" 90 | 91 | docker_manifests: 92 | - name_template: ghcr.io/trugamr/wol:{{ .Version }} 93 | image_templates: 94 | - ghcr.io/trugamr/wol:{{ .Version }}-amd64 95 | - ghcr.io/trugamr/wol:{{ .Version }}-arm64 96 | - ghcr.io/trugamr/wol:{{ .Version }}-armv7 97 | - name_template: ghcr.io/trugamr/wol:latest 98 | image_templates: 99 | - ghcr.io/trugamr/wol:latest-amd64 100 | - ghcr.io/trugamr/wol:latest-arm64 101 | - ghcr.io/trugamr/wol:latest-armv7 102 | 103 | changelog: 104 | sort: asc 105 | filters: 106 | exclude: 107 | - "^docs:" 108 | - "^test:" 109 | - "^Merge pull request" 110 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Shivam Dua 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wol 🦭 2 | 3 | A CLI tool to send Wake-On-LAN (WOL) magic packets to wake up devices on your 4 | network. Features both CLI commands and a web interface. 5 | 6 | Web Interface 7 | 8 | ## Features 9 | 10 | - Send WOL magic packets via CLI or web interface 11 | - Configure multiple machines with names for easy access 12 | - List configured machines 13 | - Web interface for easy wake-up 14 | - Docker support 15 | 16 | ## Installation 17 | 18 | ### Pre-built binaries 19 | 20 | Download the latest release for your platform from the 21 | [releases page](https://github.com/trugamr/wol/releases). 22 | 23 | Available for: 24 | 25 | - Linux (x86_64, arm64, armv7) 26 | - macOS (x86_64, arm64) 27 | - Windows (x86_64) 28 | 29 | ### Using Go 30 | 31 | ```sh 32 | go install github.com/trugamr/wol@latest 33 | ``` 34 | 35 | ### Using Docker 36 | 37 | ```sh 38 | docker run --network host -v $(pwd)/config.yaml:/etc/wol/config.yaml ghcr.io/trugamr/wol:latest 39 | ``` 40 | 41 | Or using docker-compose: 42 | 43 | ```yaml 44 | # Method 1: Using bind mount 45 | services: 46 | wol: 47 | image: ghcr.io/trugamr/wol:latest 48 | command: serve # To start the web interface 49 | network_mode: "host" 50 | volumes: 51 | - ./config.yaml:/etc/wol/config.yaml 52 | 53 | # Method 2: Using environment variables 54 | services: 55 | wol: 56 | image: ghcr.io/trugamr/wol:latest 57 | command: serve # To start the web interface 58 | network_mode: "host" 59 | environment: 60 | WOL_CONFIG: | 61 | machines: 62 | - name: desktop 63 | mac: "00:11:22:33:44:55" 64 | ip: "192.168.1.100" # Optional, for status checking 65 | - name: server 66 | mac: "AA:BB:CC:DD:EE:FF" 67 | ip: "server.local" 68 | server: 69 | listen: ":7777" # Optional, defaults to :7777 70 | ping: 71 | privileged: false # Optional, set to true to use privileged ping 72 | ``` 73 | 74 | Check out `examples/reverse-proxy.yml` for an example of running wol behind 75 | reverse proxy with basic auth, https etc. 76 | 77 | > [!NOTE] 78 | > The config file should be mounted to `/etc/wol/config.yaml` inside the 79 | > container. Host networking is recommended for Wake-on-LAN packets to work 80 | > properly on your local network. 81 | 82 | ## Configuration 83 | 84 | Create a `config.yaml` file in one of these locations (in order of precedence): 85 | 86 | - `./config.yaml` (current directory) 87 | - `~/.wol/config.yaml` (home directory) 88 | - `/etc/wol/config.yaml` (system-wide) 89 | 90 | Alternatively, you can provide the configuration via the `WOL_CONFIG` environment variable: 91 | 92 | ```sh 93 | export WOL_CONFIG=' 94 | machines: 95 | - name: desktop 96 | mac: "00:11:22:33:44:55" 97 | ip: "192.168.1.100" # Optional, for status checking 98 | - name: server 99 | mac: "AA:BB:CC:DD:EE:FF" 100 | ip: "server.local" 101 | 102 | server: 103 | listen: ":7777" # Optional, defaults to :7777 104 | ' 105 | ``` 106 | 107 | Example configuration: 108 | 109 | ```yaml 110 | machines: 111 | - name: desktop 112 | mac: "00:11:22:33:44:55" 113 | ip: "192.168.1.100" # Optional, for status checking 114 | - name: server 115 | mac: "AA:BB:CC:DD:EE:FF" 116 | ip: "server.local" 117 | 118 | server: 119 | listen: ":7777" # Optional, defaults to :7777 120 | 121 | ping: 122 | privileged: false # Optional, set to true if you need privileged ping 123 | ``` 124 | 125 | ## Usage 126 | 127 | ### CLI Commands 128 | 129 | ```sh 130 | # List all configured machines 131 | wol list 132 | 133 | # Wake up a machine by name 134 | wol send --name desktop 135 | 136 | # Wake up a machine by MAC address 137 | wol send --mac "00:11:22:33:44:55" 138 | 139 | # Start the web interface 140 | wol serve 141 | 142 | # Show version information 143 | wol version 144 | ``` 145 | 146 | ### Web Interface 147 | 148 | The web interface is available at `http://localhost:7777` when running the serve 149 | command. It provides: 150 | 151 | - List of all configured machines 152 | - One-click wake up buttons 153 | - Real-time machine status monitoring (when IP is configured) 154 | - Version information 155 | - Links to documentation and support 156 | 157 | ## Building from Source 158 | 159 | ```sh 160 | # Clone the repository 161 | git clone https://github.com/trugamr/wol.git 162 | cd wol 163 | 164 | # Build 165 | go build 166 | 167 | # Run 168 | ./wol 169 | ``` 170 | 171 | ## Known Issues 172 | 173 | ### Docker Container Ping Permissions 174 | 175 | When running in a Docker container, the machine status feature that uses ping may not work due to permission issues. This is because the application uses [pro-bing](https://github.com/prometheus-community/pro-bing) for sending pings, which requires specific Linux kernel settings. 176 | 177 | To fix this issue, you need to set the following sysctl parameter on your host system: 178 | 179 | ```sh 180 | sysctl -w net.ipv4.ping_group_range="0 2147483647" 181 | ``` 182 | 183 | To make this change persistent, add it to your `/etc/sysctl.conf` file. 184 | 185 | You can also try experimenting with setting `ping.privileged: true` in your configuration as an alternative solution. 186 | 187 | For more details, see [issue #12](https://github.com/Trugamr/wol/issues/12). 188 | 189 | ## License 190 | 191 | This project is licensed under the MIT License. See the [LICENSE](LICENSE.md) 192 | file for details. 193 | 194 | ## Contributing 195 | 196 | Contributions are welcome! Feel free to open issues or submit pull requests. 197 | -------------------------------------------------------------------------------- /assets/images/web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Trugamr/wol/3e06254c649eb2375d590828b3250732ee6632f3/assets/images/web.png -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func init() { 10 | rootCmd.AddCommand(listCmd) 11 | } 12 | 13 | var listCmd = &cobra.Command{ 14 | Use: "list", 15 | Short: "List machines from config file", 16 | Long: "Show a list of all the configured machines", 17 | Args: cobra.NoArgs, 18 | Run: func(cmd *cobra.Command, args []string) { 19 | if len(cfg.Machines) == 0 { 20 | fmt.Println("No machines configured") 21 | return 22 | } 23 | 24 | // Render the list of machines 25 | fmt.Println("Name\tMAC") 26 | for _, machine := range cfg.Machines { 27 | fmt.Printf("%s\t%s\n", machine.Name, machine.Mac) 28 | } 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | "github.com/trugamr/wol/config" 8 | ) 9 | 10 | var cfg = config.NewConfig() 11 | 12 | var rootCmd = &cobra.Command{ 13 | Use: "wol", 14 | Short: "Discover and wake up devices on the network", 15 | Long: "Discover devices on the network and wake them by sending magic Wake-On-LAN packets", 16 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 17 | return cfg.Load() 18 | }, 19 | } 20 | 21 | func Execute() { 22 | err := rootCmd.Execute() 23 | if err != nil { 24 | os.Exit(1) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /cmd/send.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "strings" 8 | 9 | "github.com/spf13/cobra" 10 | "github.com/trugamr/wol/magicpacket" 11 | ) 12 | 13 | func init() { 14 | rootCmd.AddCommand(sendCmd) 15 | 16 | sendCmd.Flags().StringP("mac", "m", "", "MAC address of the device to wake up") 17 | sendCmd.Flags().StringP("name", "n", "", "Name of the device to wake up") 18 | } 19 | 20 | var sendCmd = &cobra.Command{ 21 | Use: "send", 22 | Short: "Send a magic packet to specified mac address", 23 | Long: "Send a magic packet to wake up a device on the network using the specified mac address", 24 | Args: cobra.NoArgs, 25 | PreRunE: func(cmd *cobra.Command, args []string) error { 26 | // Only one of the flags should be specified 27 | if cmd.Flags().Changed("mac") == cmd.Flags().Changed("name") { 28 | return fmt.Errorf("either --mac or --name must be specified") 29 | } 30 | return nil 31 | }, 32 | Run: func(cmd *cobra.Command, args []string) { 33 | var mac net.HardwareAddr 34 | 35 | // Retrieve mac address using one of the flags 36 | switch true { 37 | case cmd.Flags().Changed("mac"): 38 | value, err := cmd.Flags().GetString("mac") 39 | if err != nil { 40 | cobra.CheckErr(err) 41 | } 42 | 43 | mac, err = net.ParseMAC(value) 44 | if err != nil { 45 | cobra.CheckErr(err) 46 | } 47 | case cmd.Flags().Changed("name"): 48 | // Get the name of the machine 49 | name, err := cmd.Flags().GetString("name") 50 | if err != nil { 51 | cobra.CheckErr(err) 52 | } 53 | 54 | // Find machine with the specified name 55 | mac, err = getMacByName(name) 56 | if err != nil { 57 | cobra.CheckErr(err) 58 | } 59 | default: 60 | log.Fatalf("mac address should come from either --mac or --name") 61 | } 62 | 63 | log.Printf("Sending magic packet to %s", mac) 64 | mp := magicpacket.NewMagicPacket(mac) 65 | if err := mp.Broadcast(); err != nil { 66 | cobra.CheckErr(err) 67 | } 68 | 69 | log.Printf("Magic packet sent") 70 | }, 71 | } 72 | 73 | // getMacByName returns the MAC address of the machine with the specified name 74 | func getMacByName(name string) (net.HardwareAddr, error) { 75 | for _, machine := range cfg.Machines { 76 | if strings.EqualFold(machine.Name, name) { 77 | mac, err := net.ParseMAC(machine.Mac) 78 | if err != nil { 79 | return nil, fmt.Errorf("failed to parse MAC address: %w", err) 80 | } 81 | return mac, nil 82 | } 83 | } 84 | 85 | return nil, fmt.Errorf("machine with name %q not found", name) 86 | } 87 | -------------------------------------------------------------------------------- /cmd/serve.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "embed" 5 | "encoding/json" 6 | "fmt" 7 | "html/template" 8 | "log" 9 | "net/http" 10 | "sync" 11 | "time" 12 | 13 | probing "github.com/prometheus-community/pro-bing" 14 | "github.com/spf13/cobra" 15 | "github.com/trugamr/wol/config" 16 | "github.com/trugamr/wol/magicpacket" 17 | ) 18 | 19 | //go:embed templates/* 20 | var templates embed.FS 21 | 22 | func init() { 23 | rootCmd.AddCommand(serveCmd) 24 | } 25 | 26 | var serveCmd = &cobra.Command{ 27 | Use: "serve", 28 | Short: "Serve a web interface to wake up machines", 29 | Long: "Serve a web interface that lists all the configured machines and allows you to wake them up", 30 | Args: cobra.NoArgs, 31 | Run: func(cmd *cobra.Command, args []string) { 32 | mux := http.NewServeMux() 33 | 34 | mux.HandleFunc("GET /{$}", handleIndex) 35 | mux.HandleFunc("POST /wake", handleWake) 36 | mux.HandleFunc("GET /status", handleStatus) 37 | 38 | log.Printf("Listening on %s", cfg.Server.Listen) 39 | err := http.ListenAndServe(cfg.Server.Listen, mux) 40 | if err != nil { 41 | cobra.CheckErr(err) 42 | } 43 | }, 44 | } 45 | 46 | func handleIndex(w http.ResponseWriter, r *http.Request) { 47 | // Parse the template 48 | index, err := template.ParseFS(templates, "templates/index.html") 49 | if err != nil { 50 | log.Printf("Error parsing template: %v", err) 51 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 52 | return 53 | } 54 | 55 | // Execute the template 56 | data := map[string]interface{}{ 57 | "Machines": cfg.Machines, 58 | "Version": version, 59 | "Commit": commit, 60 | "Date": date, 61 | "FlashMessage": consumeFlashMessage(w, r), // Get flash message from cookie 62 | } 63 | err = index.Execute(w, data) 64 | if err != nil { 65 | log.Printf("Error executing template: %v", err) 66 | http.Error(w, "Internal Server Error", http.StatusInternalServerError) 67 | return 68 | } 69 | } 70 | 71 | // setFlashMessage sets a flash message in a cookie 72 | func setFlashMessage(w http.ResponseWriter, message string) { 73 | http.SetCookie(w, &http.Cookie{ 74 | Name: "flash", 75 | Value: message, 76 | Path: "/", 77 | }) 78 | } 79 | 80 | // consumeFlashMessage retrieves and clears the flash message from the request 81 | func consumeFlashMessage(w http.ResponseWriter, r *http.Request) string { 82 | cookie, err := r.Cookie("flash") 83 | if err == nil { 84 | // Clear the cookie 85 | http.SetCookie(w, &http.Cookie{ 86 | Name: "flash", 87 | Value: "", 88 | Path: "/", 89 | Expires: time.Now().Add(-1 * time.Hour), 90 | }) 91 | 92 | return cookie.Value 93 | } 94 | return "" 95 | } 96 | 97 | func handleWake(w http.ResponseWriter, r *http.Request) { 98 | machineName := r.FormValue("name") 99 | mac, err := getMacByName(machineName) 100 | if err != nil { 101 | http.Error(w, err.Error(), http.StatusBadRequest) 102 | return 103 | } 104 | 105 | log.Printf("Sending magic packet to %s", mac) 106 | mp := magicpacket.NewMagicPacket(mac) 107 | if err := mp.Broadcast(); err != nil { 108 | log.Printf("Error sending magic packet: %v", err) 109 | http.Error(w, err.Error(), http.StatusInternalServerError) 110 | return 111 | } 112 | 113 | // Set flash message cookie 114 | setFlashMessage(w, fmt.Sprintf("Wake-up signal sent to %s. The machine should wake up shortly.", machineName)) 115 | 116 | http.Redirect(w, r, "/", http.StatusSeeOther) 117 | } 118 | 119 | // getMachineStatus returns the status of a machine 120 | func getMachineStatus(machine config.Machine) (string, error) { 121 | if machine.IP == nil { 122 | return "unknown", nil 123 | } 124 | 125 | reachable, err := isAddressReachable(*machine.IP) 126 | if err != nil { 127 | return "unknown", err 128 | } 129 | if reachable { 130 | return "online", nil 131 | } 132 | 133 | return "offline", nil 134 | } 135 | 136 | // getMachinesStatus returns a map of machine names to their statuses concurrently 137 | func getMachinesStatus() map[string]string { 138 | var mu sync.Mutex 139 | statuses := make(map[string]string) 140 | var wg sync.WaitGroup 141 | 142 | for _, machine := range cfg.Machines { 143 | wg.Add(1) 144 | go func(machine config.Machine) { 145 | defer wg.Done() 146 | status, err := getMachineStatus(machine) 147 | if err != nil { 148 | log.Printf("Error getting status for machine %s: %v", machine.Name, err) 149 | return 150 | } 151 | 152 | mu.Lock() 153 | statuses[machine.Name] = status 154 | mu.Unlock() 155 | }(machine) 156 | } 157 | 158 | wg.Wait() 159 | 160 | return statuses 161 | } 162 | 163 | func handleStatus(w http.ResponseWriter, r *http.Request) { 164 | w.Header().Set("Content-Type", "text/event-stream") 165 | w.Header().Set("Cache-Control", "no-cache") 166 | w.Header().Set("Connection", "keep-alive") 167 | 168 | // Sends the current status of all machines 169 | sendMachinesStatus := func() { 170 | statuses := getMachinesStatus() 171 | data, err := json.Marshal(statuses) 172 | if err != nil { 173 | log.Printf("Error marshaling status: %v", err) 174 | return 175 | } 176 | 177 | _, err = fmt.Fprintf(w, "data: %s\n\n", data) 178 | if err != nil { 179 | log.Printf("Error writing status: %v", err) 180 | return 181 | } 182 | 183 | w.(http.Flusher).Flush() 184 | } 185 | 186 | // Sends initial status 187 | sendMachinesStatus() 188 | 189 | // Send status updates every few seconds 190 | ticker := time.NewTicker(5 * time.Second) 191 | defer ticker.Stop() 192 | 193 | for { 194 | select { 195 | case <-r.Context().Done(): 196 | return 197 | case <-ticker.C: 198 | sendMachinesStatus() 199 | } 200 | } 201 | } 202 | 203 | func isAddressReachable(addr string) (bool, error) { 204 | pinger, err := probing.NewPinger(addr) 205 | if err != nil { 206 | return false, fmt.Errorf("error creating pinger: %v", err) 207 | } 208 | // Set privileged mode based on config 209 | pinger.SetPrivileged(cfg.Ping.Privileged) 210 | 211 | // We only want to ping once and wait 2 seconds for a response 212 | pinger.Timeout = 2 * time.Second 213 | pinger.Count = 1 214 | 215 | err = pinger.Run() 216 | if err != nil { 217 | return false, fmt.Errorf("error pinging: %v", err) 218 | } 219 | 220 | // If we receive even a single packet, the address is reachable 221 | stats := pinger.Statistics() 222 | if stats.PacketsRecv == 0 { 223 | return false, nil 224 | } 225 | 226 | return true, nil 227 | } 228 | -------------------------------------------------------------------------------- /cmd/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | wol 8 | 256 | 257 | 258 |
259 | {{if .FlashMessage}} 260 |
261 | {{.FlashMessage}} 262 |
263 | {{end}} 264 |

wol

265 |

Wake-on-LAN web interface

266 | {{if .Machines}} 267 |

Machines

268 |

List of configured machines and their current status

269 | 286 | {{else}} 287 |
288 |
🖥️
289 |

No machines configured

290 |

291 | Add machines to your configuration file to start using Wake-on-LAN. 292 | Check the documentation for setup instructions. 293 |

294 |
295 | {{end}} 296 |
297 | 306 | 330 | 331 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var ( 10 | // These variables are set during build time using -ldflags 11 | version = "dev" 12 | commit = "none" 13 | date = "unknown" 14 | ) 15 | 16 | func init() { 17 | rootCmd.AddCommand(versionCmd) 18 | } 19 | 20 | var versionCmd = &cobra.Command{ 21 | Use: "version", 22 | Short: "Print version information", 23 | Long: "Print detailed version information about the wol CLI", 24 | Run: func(cmd *cobra.Command, args []string) { 25 | fmt.Printf("wol %s\n", version) 26 | fmt.Printf(" commit: %s\n", commit) 27 | fmt.Printf(" built: %s\n", date) 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /config.example.yaml: -------------------------------------------------------------------------------- 1 | machines: 2 | - name: desktop 3 | mac: 00:00:00:00:00:01 4 | - name: laptop 5 | mac: 00:00:00:00:00:02 6 | server: 7 | listen: 0.0.0.0:7777 8 | # Check: https://github.com/prometheus-community/pro-bing?tab=readme-ov-file#supported-operating-systems 9 | ping: 10 | privileged: false -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/knadh/koanf/parsers/yaml" 9 | "github.com/knadh/koanf/providers/file" 10 | "github.com/knadh/koanf/providers/rawbytes" 11 | "github.com/knadh/koanf/providers/structs" 12 | "github.com/knadh/koanf/v2" 13 | ) 14 | 15 | const ( 16 | koanfDelimiter = "." 17 | koanfTag = "koanf" 18 | configFilename = "config.yaml" 19 | ) 20 | 21 | var k = koanf.New(koanfDelimiter) 22 | 23 | // Machine represents a machine to wake up 24 | type Machine struct { 25 | // Name of the machine 26 | Name string `koanf:"name"` 27 | // MAC address of the machine 28 | Mac string `koanf:"mac"` 29 | // Hostname or IP address of the machine (optional) 30 | IP *string `koanf:"ip"` 31 | } 32 | 33 | // Server represents the server configuration 34 | type Server struct { 35 | // Listen address for the server 36 | Listen string `koanf:"listen"` 37 | } 38 | 39 | // Ping represents the ping configuration 40 | type Ping struct { 41 | // Privileged determines if privileged ping should be used 42 | Privileged bool `koanf:"privileged"` 43 | } 44 | 45 | // Config represents the configuration for the application 46 | type Config struct { 47 | // Machines represents the list of machines to wake up 48 | Machines []Machine `koanf:"machines"` 49 | // Server represents the server configuration 50 | Server Server `koanf:"server"` 51 | // Ping represents the ping configuration 52 | Ping Ping `koanf:"ping"` 53 | } 54 | 55 | // NewConfig creates a new Config instance 56 | func NewConfig() *Config { 57 | return &Config{} 58 | } 59 | 60 | // Load loads the configuration from the config file 61 | // 62 | // Configuration is loaded in the following order (later values override earlier ones): 63 | // 1. Default values 64 | // 2. Config files from: 65 | // - /etc/wol/config.yaml 66 | // - ~/.wol/config.yaml 67 | // - ./config.yaml 68 | // 69 | // 3. Environment variable `WOL_CONFIG` containing full YAML config 70 | func (c *Config) Load() error { 71 | // Load defaults first 72 | defaults := &Config{ 73 | Server: Server{ 74 | Listen: ":7777", 75 | }, 76 | Ping: Ping{ 77 | Privileged: false, 78 | }, 79 | } 80 | err := k.Load(structs.Provider(defaults, koanfTag), nil) 81 | if err != nil { 82 | return fmt.Errorf("failed to load defaults: %w", err) 83 | } 84 | 85 | home, err := os.UserHomeDir() 86 | if err != nil { 87 | return fmt.Errorf("failed to get home directory: %w", err) 88 | } 89 | 90 | // Order here matters as later values will override earlier ones 91 | paths := []string{ 92 | filepath.Join("/etc", "wol", configFilename), 93 | filepath.Join(home, ".wol", configFilename), 94 | filepath.Join(".", configFilename), 95 | } 96 | 97 | for _, path := range paths { 98 | err = k.Load(file.Provider(path), yaml.Parser()) 99 | 100 | // Ignore error if file does not exist 101 | if err != nil && !os.IsNotExist(err) { 102 | return fmt.Errorf("failed to load config file: %w", err) 103 | } 104 | } 105 | 106 | // Load from `WOL_CONFIG` environment variable if set 107 | ec := []byte(os.Getenv("WOL_CONFIG")) 108 | err = k.Load(rawbytes.Provider(ec), yaml.Parser()) 109 | if err != nil { 110 | return fmt.Errorf("failed to load config from WOL_CONFIG: %w", err) 111 | } 112 | 113 | err = k.Unmarshal("", c) 114 | if err != nil { 115 | return fmt.Errorf("failed to unmarshal config: %w", err) 116 | } 117 | 118 | return nil 119 | } 120 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22.3-alpine3.20 AS builder 2 | 3 | WORKDIR /app 4 | 5 | COPY go.mod go.sum ./ 6 | RUN go mod download 7 | 8 | COPY . . 9 | RUN go build -o wol 10 | 11 | #------------------# 12 | 13 | FROM gcr.io/distroless/static-debian12 AS release 14 | 15 | COPY --from=builder /app/wol /usr/local/bin/wol 16 | 17 | USER nonroot:nonroot 18 | 19 | ENTRYPOINT ["/usr/local/bin/wol"] -------------------------------------------------------------------------------- /docker/Dockerfile.template: -------------------------------------------------------------------------------- 1 | # This Dockerfile is used to build docker image using goreleaser. 2 | FROM gcr.io/distroless/static-debian12 3 | 4 | COPY wol /usr/local/bin/ 5 | 6 | ENTRYPOINT ["wol"] -------------------------------------------------------------------------------- /examples/reverse-proxy.yml: -------------------------------------------------------------------------------- 1 | # This example shows how to run wol behind a reverse proxy without making is accessible via port from outside by listening only on the bridge network. 2 | # This can be extended to use any traefik features like TLS, rate limiting, etc. 3 | 4 | name: wol 5 | 6 | services: 7 | traefik: 8 | image: traefik:v3.3.4 9 | command: 10 | - --providers.docker=true 11 | - --providers.docker.exposedbydefault=false 12 | - --entrypoints.web.address=:80 13 | ports: 14 | - 80:80 15 | volumes: 16 | - /var/run/docker.sock:/var/run/docker.sock:ro 17 | extra_hosts: 18 | # Map host.docker.internal to bridge network 19 | - host.docker.internal:172.17.0.1 20 | 21 | wol: 22 | image: ghcr.io/trugamr/wol:latest 23 | network_mode: host 24 | command: serve 25 | environment: 26 | WOL_CONFIG: | 27 | machines: 28 | - name: desktop 29 | mac: e3:2e:4a:00:00:01 30 | server: 31 | # Listen only on the bridge network 32 | listen: 172.17.0.1:7777 33 | labels: 34 | traefik.enable: true 35 | # Match every host and path 36 | # https://doc.traefik.io/traefik/routing/routers/#rule 37 | traefik.http.routers.wol.rule: PathPrefix(`/`) 38 | traefik.http.routers.wol.entrypoints: web 39 | # Create and add basic auth middleware 40 | # Generate credentials using `htpasswd -nb username password` 41 | # Note: Escaping $ in password with $$ maybe required 42 | traefik.http.middlewares.auth.basicauth.users: "admin:$$apr1$$Qb.3XKXp$$zR5u/t.DrvejOkMIsCSYJ0" 43 | traefik.http.routers.wol.middlewares: auth 44 | traefik.http.services.wol.loadbalancer.server.port: 7777 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/trugamr/wol 2 | 3 | go 1.22.3 4 | 5 | require ( 6 | github.com/knadh/koanf/parsers/yaml v0.1.0 7 | github.com/knadh/koanf/providers/file v1.1.2 8 | github.com/knadh/koanf/providers/rawbytes v0.1.0 9 | github.com/knadh/koanf/providers/structs v0.1.0 10 | github.com/knadh/koanf/v2 v2.1.2 11 | github.com/prometheus-community/pro-bing v0.5.0 12 | github.com/spf13/cobra v1.8.1 13 | ) 14 | 15 | require ( 16 | github.com/fatih/structs v1.1.0 // indirect 17 | github.com/fsnotify/fsnotify v1.8.0 // indirect 18 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 19 | github.com/google/uuid v1.6.0 // indirect 20 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 21 | github.com/knadh/koanf/maps v0.1.1 // indirect 22 | github.com/kr/pretty v0.2.1 // indirect 23 | github.com/kr/text v0.2.0 // indirect 24 | github.com/mitchellh/copystructure v1.2.0 // indirect 25 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 26 | github.com/spf13/pflag v1.0.5 // indirect 27 | github.com/stretchr/testify v1.8.4 // indirect 28 | golang.org/x/net v0.34.0 // indirect 29 | golang.org/x/sync v0.10.0 // indirect 30 | golang.org/x/sys v0.29.0 // indirect 31 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 32 | gopkg.in/yaml.v3 v3.0.1 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 6 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 7 | github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= 8 | github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 9 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 10 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 11 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 12 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 13 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 14 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 15 | github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= 16 | github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 17 | github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= 18 | github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= 19 | github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w= 20 | github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= 21 | github.com/knadh/koanf/providers/rawbytes v0.1.0 h1:dpzgu2KO6uf6oCb4aP05KDmKmAmI51k5pe8RYKQ0qME= 22 | github.com/knadh/koanf/providers/rawbytes v0.1.0/go.mod h1:mMTB1/IcJ/yE++A2iEZbY1MLygX7vttU+C+S/YmPu9c= 23 | github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSdntfdyIbbCzEyE0= 24 | github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE= 25 | github.com/knadh/koanf/v2 v2.1.2 h1:I2rtLRqXRy1p01m/utEtpZSSA6dcJbgGVuE27kW2PzQ= 26 | github.com/knadh/koanf/v2 v2.1.2/go.mod h1:Gphfaen0q1Fc1HTgJgSTC4oRX9R2R5ErYMZJy8fLJBo= 27 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 28 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 29 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 30 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 31 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 32 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 33 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 34 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 35 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 36 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 37 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 38 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 39 | github.com/prometheus-community/pro-bing v0.5.0 h1:Fq+4BUXKIvsPtXUY8K+04ud9dkAuFozqGmRAyNUpffY= 40 | github.com/prometheus-community/pro-bing v0.5.0/go.mod h1:1joR9oXdMEAcAJJvhs+8vNDvTg5thfAZcRFhcUozG2g= 41 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 42 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 43 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 44 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 45 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 46 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 47 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 48 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 49 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 50 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 51 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 52 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 53 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 54 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 55 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 56 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 57 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 58 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 59 | -------------------------------------------------------------------------------- /magicpacket/magicpacket.go: -------------------------------------------------------------------------------- 1 | package magicpacket 2 | 3 | import "net" 4 | 5 | // MagicPacket represents a wake-on-LAN packet 6 | type MagicPacket struct { 7 | // The MAC address of the machine to wake up 8 | MacAddress net.HardwareAddr 9 | } 10 | 11 | // NewMagicPacket creates a new MagicPacket for the given MAC address 12 | func NewMagicPacket(macAddress net.HardwareAddr) *MagicPacket { 13 | return &MagicPacket{MacAddress: macAddress} 14 | } 15 | 16 | // Broadcast sends the magic packet to the broadcast address 17 | func (p *MagicPacket) Broadcast() error { 18 | // Build the actual packet 19 | packet := make([]byte, 102) 20 | // Set the synchronization stream (first 6 bytes are 0xFF) 21 | for i := 0; i < 6; i++ { 22 | packet[i] = 0xFF 23 | } 24 | // Copy the MAC address 16 times into the packet 25 | for i := 1; i <= 16; i++ { 26 | copy(packet[i*6:], p.MacAddress) 27 | } 28 | 29 | // Broadcast the packet 30 | // TODO: Broadcast to more common ports and addresses? 31 | addr := &net.UDPAddr{ 32 | IP: net.IPv4bcast, 33 | Port: 9, 34 | } 35 | conn, err := net.DialUDP("udp", nil, addr) 36 | if err != nil { 37 | return err 38 | } 39 | defer conn.Close() 40 | 41 | _, err = conn.Write(packet) 42 | return err 43 | } 44 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/trugamr/wol/cmd" 5 | ) 6 | 7 | func main() { 8 | // Initialize root command 9 | cmd.Execute() 10 | } 11 | --------------------------------------------------------------------------------