blob: b98a3cce713903f097e876e012989d15c4bb7a7a [file] [log] [blame]
// Copyright 2017 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// This package implements the Zircon netboot protocol.
package netboot
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"net"
"strings"
"time"
)
// Magic constants used by the netboot protocol.
const (
magic = 0xAA774217 // see system/public/zircon/boot/netboot.h
)
// Port numbers used by the netboot protocol.
const (
serverPort = 33330 // netboot server port
advertPort = 33331 // advertisement port
)
// Commands supported by the netboot protocol.
const (
cmdCommand = uint32(1) // command
cmdSendFile = uint32(2) // send file
cmdData = uint32(3) // data
cmdBoot = uint32(4) // boot command
cmdQuery = uint32(5) // query command
cmdShell = uint32(6) // shell command
cmdOpen = uint32(7) // open file
cmdRead = uint32(8) // read data
cmdWrite = uint32(9) // write data
cmdClose = uint32(10) // close file
cmdLastData = uint32(11) //
cmdReboot = uint32(12) // reboot command
)
// Client implements the netboot protocol.
type Client struct {
Port int
Cookie uint32
Timeout time.Duration
Wait bool
}
// netbootHeader is the netboot protocol message header.
type netbootHeader struct {
Magic uint32
Cookie uint32
Cmd uint32
Arg uint32
}
// netbootMessage is the netboot protocol message.
type netbootMessage struct {
Header netbootHeader
Data [1024]byte
}
// NewClient creates a new Client instance.
func NewClient(timeout time.Duration) *Client {
return &Client{
Timeout: timeout,
Cookie: uint32(0x12345678),
}
}
// Discover resolves the address of host and returns either the netsvc or
// Fuchsia address dependending on the value of fuchsia.
func (n *Client) Discover(nodename string, fuchsia bool) (*net.UDPAddr, error) {
conn, err := net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6zero})
if err != nil {
return nil, fmt.Errorf("failed to bind to udp6 port: %v\n", err)
}
defer conn.Close()
n.Cookie++
req := netbootMessage{
Header: netbootHeader{
Magic: magic,
Cookie: n.Cookie,
Cmd: cmdQuery,
Arg: 0,
},
}
copy(req.Data[:], nodename)
var buf bytes.Buffer
if err := binary.Write(&buf, binary.LittleEndian, req); err != nil {
return nil, err
}
// Enumerate all available network interfaces.
ifaces, err := net.Interfaces()
if err != nil {
return nil, fmt.Errorf("failed to enumerate network interfaces: %v\n", err)
}
for _, iface := range ifaces {
// Skip interfaces that are down.
if iface.Flags&net.FlagUp == 0 {
continue
}
// Skip loopback interfaces.
if iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
return nil, err
}
// Enumerate all interface addresses and find the usable ones.
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.To16() == nil {
continue
}
// Send a broadcast query command using the interface.
conn.WriteToUDP(buf.Bytes(), &net.UDPAddr{
IP: net.IPv6linklocalallnodes,
Port: serverPort,
Zone: iface.Name,
})
}
}
// Wait for response, this is a best effort approach so we may not receive
// any response if there're no usable interfaces or no devices connected.
conn.SetReadDeadline(time.Now().Add(n.Timeout))
b := make([]byte, 4096)
_, addr, err := conn.ReadFromUDP(b)
if err != nil {
return nil, err
}
r := bytes.NewReader(b)
var res netbootMessage
if err := binary.Read(r, binary.LittleEndian, &res); err != nil {
return nil, err
}
data, err := netbootString(res.Data[:])
if err != nil {
return nil, err
}
// The query packet payload contains the nodename.
if data != nodename {
return nil, fmt.Errorf("invalid nodename `%s`", data)
}
if fuchsia {
// The netstack link-local address has 11th byte always set to 0xff, set
// this byte to transform netsvc address to netstack address if needed.
addr.IP[11] = 0xff
}
return addr, nil
}
// Beacon receives the beacon packet, returning the address of the sender.
func (n *Client) Beacon() (*net.UDPAddr, error) {
conn, err := net.ListenUDP("udp6", &net.UDPAddr{
IP: net.IPv6zero,
Port: advertPort,
})
defer conn.Close()
conn.SetReadDeadline(time.Now().Add(n.Timeout))
b := make([]byte, 4096)
_, addr, err := conn.ReadFromUDP(b)
if err != nil {
return nil, err
}
r := bytes.NewReader(b)
var res netbootMessage
if err := binary.Read(r, binary.LittleEndian, &res); err != nil {
return nil, err
}
data, err := netbootString(res.Data[:])
if err != nil {
return nil, err
}
// The query packet payload contains fields separated by ;.
for _, f := range strings.Split(string(data[:]), ";") {
// The field has a key=value format.
vars := strings.SplitN(f, "=", 2)
// The field with the "nodename" key contains the name of the device.
if vars[0] == "nodename" {
return addr, nil
}
}
return nil, errors.New("no valid beacon")
}
// Boot sends a boot packet to the address.
func (n *Client) Boot(addr *net.UDPAddr) error {
n.Cookie++
msg := &netbootHeader{
Magic: magic,
Cookie: n.Cookie,
Cmd: cmdBoot,
Arg: 0,
}
if err := sendPacket(msg, addr); err != nil {
return fmt.Errorf("failed to send boot command: %v\n", err)
}
return nil
}
// Reboot sends a reboot packet the address.
func (n *Client) Reboot(addr *net.UDPAddr) error {
n.Cookie++
msg := &netbootHeader{
Magic: magic,
Cookie: n.Cookie,
Cmd: cmdReboot,
Arg: 0,
}
if err := sendPacket(msg, addr); err != nil {
return fmt.Errorf("failed to send reboot command: %v\n", err)
}
return nil
}
func sendPacket(msg *netbootHeader, addr *net.UDPAddr) error {
if msg == nil {
return errors.New("no message provided")
}
var buf bytes.Buffer
if err := binary.Write(&buf, binary.LittleEndian, *msg); err != nil {
return err
}
conn, err := net.ListenUDP("udp6", &net.UDPAddr{IP: net.IPv6zero})
if err != nil {
return fmt.Errorf("failed to create a socket: %v\n", err)
}
defer conn.Close()
_, err = conn.WriteToUDP(buf.Bytes(), &net.UDPAddr{
IP: addr.IP,
Port: serverPort,
Zone: addr.Zone,
})
return err
}
func netbootString(bs []byte) (string, error) {
for i, b := range bs {
if b == 0 {
return string(bs[:i]), nil
}
}
return "", errors.New("no null terminated string found")
}