Control your backup NAS from the desktop
Programming Snapshot – Remote Backup
To be able to power up and shut down his NAS and check the current status without getting out of his chair, Mike Schilli programs a graphical interface that sends a Magic Packet in this month's column.
As a backup solution, I use a Synology NAS with some hefty hard drives. But because of the strict regulations driven by paranoia and noise-protection goals in the hallowed halls of Perlmeister Studios, the device only runs when actually needed (i.e., when a backup is running). To switch it on when needed, I prefer to push a mouse around a GUI and click occasionally instead of getting out of my chair.
The Syno desktop application presented here (Figure 1) powers up the NAS via the local network at the push of a button and graphically displays the milestones during the boot process with progress bars. As soon as the system has finished booting and is ready for access, it notifies the user.
After the work is done, a mouse click on the GUI's Down button is all it takes, and the NAS receives the command to shut down via the network. While the shutdown is running, the GUI checks whether the NAS is still operational or if it is no longer responding to pings and has finally gone to sleep (Figure 2).
Just Switch It On
How does this magic work? Even when switched off, the NAS actively waits for a Wake-on-LAN (WoL) signal on the local network. Despite its control lights being off, the network card is running in a low power mode. If a matching broadcast packet arrives via the LAN, it tells the device's power supply or motherboard to switch on. The Magic Packet sent in this case contains the target system's MAC address, so that a controlling transmitter can notify different network nodes independently.
Figure 3 shows a practical example of the Magic Packet's format [1]. The first 6 bytes of the packet header each contain a fixed value, 0xFF
. This is followed by the payload, which consists of 16 repetitions of the 6-byte MAC address of the target device (00:11:32:6c:ab:cd
in this example). Each network card has its own setting, which specifies the device's manufacturer, model, and individual identifier.
Listing 1 cobbles together the Magic Packet in Go. Line 7 sets the NAS's MAC address as a string, while line 8 specifies that its length really is 6 bytes. In rare cases, there could be devices with longer MAC addresses, but their Magic Packets are more difficult to build. For the purpose of illustration, I'll keep things short and sweet.
Listing 1
wol.go
01 package main 02 import ( 03 "bytes" 04 "encoding/binary" 05 "net" 06 ) 07 const synMAC = "00:11:32:6c:ab:cd" 08 const MacLen = 6 09 type MagicPacket struct { 10 header [6]byte 11 payload [16][MacLen]byte 12 } 13 func sendMagicPacket() { 14 var packet MagicPacket 15 hwAddr, err := net.ParseMAC(synMAC) 16 if err != nil { 17 panic(err) 18 } 19 for idx := range packet.header { 20 packet.header[idx] = 0xFF 21 } 22 for idx := range packet.payload { 23 for i := 0; i < MacLen; i++ { 24 packet.payload[idx][i] = hwAddr[i] 25 } 26 } 27 buf := new(bytes.Buffer) 28 if err := binary.Write(buf, binary.BigEndian, packet); err != nil { 29 panic(err) 30 } 31 conn, err := net.Dial("udp", "255.255.255.255:9") 32 if err != nil { 33 panic(err) 34 } 35 defer conn.Close() 36 _, err = conn.Write(buf.Bytes()) 37 if err != nil { 38 panic(err) 39 } 40 }
The MagicPacket
structure starting in line 9 abstracts the packet's two different areas: the header and payload. The sendMagicPacket()
function starting in line 13 then wraps the packet and, when done, sends it to the broadcast address 255.255.255.255
on the LAN. This means that all devices on the local network get to see the packet and can react accordingly. If a device listening via WoL receives a packet in which the packaged MAC address matches its own, it initiates the boot process.
The ParseMac()
function from the standard net Go library treasure trove translates the MAC string from line 7 into a binary hardware address, which the library's network functions need in order to send off the packet. The packet header with 6 0xFF
bytes is assembled by the for
loop starting in line 19. The subsequent double loop starting in line 22 then writes the MAC address in binary format 16 times in succession to the packet's payload area.
To convert the Go structure of the MagicPacket
type to a binary stream of bytes for packet recipients listening on the network, the standard binary.Write()
function in line 28 trawls the structure of the packet
variable. To do this, it writes the bytes of the structure in network format (big-endian, most significant byte first) to the buf
buffer. Line 36 then uses Write()
to send the buffer content via the UDP socket opened in line 31 by net.Dial()
to the LAN's broadcast address.
Be careful: binary.Write()
can only serialize a structure without error if all of its fields are of a fixed length. The function does not support Go's dynamically expandable array slices. You will see some really ugly runtime errors if you proceed without heeding this warning.
Panic
The states the application can be in at runtime are those of a simple finite machine. After starting the program, the NAS is usually asleep (state DOWN). The user issues the wake-up command by pressing the Up button. Following the bootstrap, the application keeps checking if the NAS can be pinged yet. If it shows no response, it is probably still asleep (i.e., resting in the DOWN state). But as soon as the ping
command reports success, the NAS is ready for operation and the finite machine jumps to the UP state.
Figure 4 shows the state machine diagram, while Listing 2 contains the implementation using the Go fsm library from GitHub. The NewFSM
function creates a new finite state machine starting in line 8 that processes two events: wake
(line 11), which transitions from the DOWN state to the UP state, and sleep
(line 12), which switches the machine from UP to DOWN. Listing 2 defines the conditions for these transitions in the enter_UP
and enter_DOWN
callbacks; the machine jumps to each of these before actually starting a transition.
Listing 2
fsm.go
01 package main 02 import ( 03 "context" 04 "time" 05 "github.com/looplab/fsm" 06 ) 07 func run(stateReporter chan string, startState string) *fsm.FSM { 08 boot := fsm.NewFSM( 09 startState, 10 fsm.Events{ 11 {Name: "wake", Src: []string{"DOWN"}, Dst: "UP"}, 12 {Name: "sleep", Src: []string{"UP"}, Dst: "DOWN"}, 13 }, 14 fsm.Callbacks{ 15 "enter_UP": func(ctx context.Context, e *fsm.Event) { 16 for { 17 if isPingable() { 18 stateReporter <- "UP" 19 return 20 } 21 time.Sleep(1 * time.Second) 22 } 23 }, 24 "enter_DOWN": func(_ context.Context, e *fsm.Event) { 25 for { 26 if !isPingable() { 27 stateReporter <- "DOWN" 28 return 29 } 30 time.Sleep(1 * time.Second) 31 } 32 }, 33 }, 34 ) 35 return boot 36 }
Each of these two callbacks erects a hurdle that the program flow needs to clear before entering the new state. An infinite for
loop keeps running isPingable()
to check whether the desired state has already been reached, in which the NAS is either running or asleep, depending on the callback. If not, the callbacks wait a second and then try again. This is repeated until the desired state is reached.
Then the callbacks send a message with the new state of the machine on the stateReporter
channel provided by the caller earlier. The run()
function's final act is to return a reference for the ready-to-go state machine to the caller. With this handy tool at its disposal, the caller will be sending new commands to the machine using methods such as Event()
to initiate the associated state transitions.
Interestingly, the third-party library used for the fsm library from GitHub uses strings instead of typed variables for its states. This is not recommended Go style, because it means that a simple typo in a state in the code is enough to trigger a frantic search for runtime errors. This way, the type checker in the Go compiler has no way to detect the bug at compile time. The library clearly still has room for improvement, but you can't look a gift horse in the mouth.
On Screen!
Listing 3 has all the code to draw the small GUI shown in the screenshots. It is based on the Fyne framework, which the code pulls in from GitHub at compile time.
Listing 3
syno.go
01 package main 02 import ( 03 "context" 04 "fyne.io/fyne/v2" 05 "fyne.io/fyne/v2/app" 06 "fyne.io/fyne/v2/canvas" 07 "fyne.io/fyne/v2/container" 08 "fyne.io/fyne/v2/theme" 09 "fyne.io/fyne/v2/widget" 10 "os" 11 ) 12 func main() { 13 state := "DOWN" 14 headText := "NAS Control Center" 15 a := app.New() 16 w := a.NewWindow(headText) 17 status := widget.NewLabelWithStyle(state, fyne.TextAlignCenter, fyne.TextStyle{Bold: true}) 18 progress := widget.NewProgressBarInfinite() 19 progress.Stop() 20 progress.Hide() 21 okIcon := widget.NewIcon(theme.ConfirmIcon()) 22 okIcon.Hide() 23 downIcon := widget.NewIcon(theme.CancelIcon()) 24 stateReporter := make(chan string) 25 runner := run(stateReporter, state) 26 var upButton *widget.Button 27 var downButton *widget.Button 28 upButton = widget.NewButton("Up", func() { 29 upButton.Disable() 30 downButton.Disable() 31 status.Text = "Coming up ..." 32 status.Refresh() 33 sendMagicPacket() 34 progress.Show() 35 go func() { 36 runner.Event(context.Background(), "wake") 37 }() 38 }) 39 downButton = widget.NewButton("Down", func() { 40 upButton.Disable() 41 downButton.Disable() 42 status.Text = "Going down ..." 43 status.Refresh() 44 progress.Show() 45 shutdownNAS() 46 go func() { 47 runner.Event(context.Background(), "sleep") 48 }() 49 }) 50 downButton.Disable() 51 go func() { 52 for { 53 select { 54 case newState := <-stateReporter: 55 progress.Hide() 56 switch newState { 57 case "DOWN": 58 okIcon.Hide() 59 downIcon.Show() 60 upButton.Enable() 61 case "UP": 62 okIcon.Show() 63 downIcon.Hide() 64 downButton.Enable() 65 } 66 status.Text = newState 67 status.Refresh() 68 } 69 } 70 }() 71 img := canvas.NewImageFromResource(nil) 72 img.SetMinSize( 73 fyne.NewSize(400, 0)) 74 grid := container.NewVBox( 75 img, 76 status, 77 okIcon, 78 downIcon, 79 progress, 80 container.NewHBox( 81 upButton, 82 downButton, 83 widget.NewButton("Quit", func() { 84 os.Exit(0) 85 }), 86 ), 87 ) 88 w.SetContent(grid) 89 w.ShowAndRun() 90 }
The app features an application window with a label widget that displays the NAS status (UP or DOWN). In addition, there is an icon (check mark if the NAS is operational; X if not) and three buttons for user control. A progress bar also appears during the transition phases. After starting the program, initially only the Up button is active, while the Down button is grayed out (Figure 5). If you press a button to initiate an action, the app grays all buttons to prevent further impatient clicking triggering confusing actions.
The application causes some parts of the GUI to change dynamically with the program flow by making certain widgets disappear in some situations (the Hide()
function is used for this) and reappear later on using Show()
. The NAS status icon – either a check mark or an X – actually consists of two separate widgets okIcon
and downIcon
, but the app only displays one of them at any given time and keeps the other one tucked away invisibly.
The infinite progress bar is also always present in the application window. However, it is only visible and moving if an action is currently running (e.g., in the callback of the Up button starting in line 28). After line 33 calls sendMagicPacket()
to send the packet that starts the NAS via the network, line 34 calls progress.Show()
to display the progress bar. If the state of the machine changes, line 55 uses Hide()
to make the progress bar visually disappear from the app again.
Buy this article as PDF
(incl. VAT)
Buy Linux Magazine
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters
Support Our Work
Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.
News
-
Gnome OS Transitioning Toward a General-Purpose Distro
If you're looking for the perfectly vanilla take on the Gnome desktop, Gnome OS might be for you.
-
Fedora 41 Released with New Features
If you're a Fedora fan or just looking for a Linux distribution to help you migrate from Windows, Fedora 41 might be just the ticket.
-
AlmaLinux OS Kitten 10 Gives Power Users a Sneak Preview
If you're looking to kick the tires of AlmaLinux's upstream version, the developers have a purrfect solution.
-
Gnome 47.1 Released with a Few Fixes
The latest release of the Gnome desktop is all about fixing a few nagging issues and not about bringing new features into the mix.
-
System76 Unveils an Ampere-Powered Thelio Desktop
If you're looking for a new desktop system for developing autonomous driving and software-defined vehicle solutions. System76 has you covered.
-
VirtualBox 7.1.4 Includes Initial Support for Linux kernel 6.12
The latest version of VirtualBox has arrived and it not only adds initial support for kernel 6.12 but another feature that will make using the virtual machine tool much easier.
-
New Slimbook EVO with Raw AMD Ryzen Power
If you're looking for serious power in a 14" ultrabook that is powered by Linux, Slimbook has just the thing for you.
-
The Gnome Foundation Struggling to Stay Afloat
The foundation behind the Gnome desktop environment is having to go through some serious belt-tightening due to continued financial problems.
-
Thousands of Linux Servers Infected with Stealth Malware Since 2021
Perfctl is capable of remaining undetected, which makes it dangerous and hard to mitigate.
-
Halcyon Creates Anti-Ransomware Protection for Linux
As more Linux systems are targeted by ransomware, Halcyon is stepping up its protection.