unix: export Ifreq and add IoctlIfreq function

This CL expands upon CL 340369 by exporting the Ifreq type along with methods
for setting and getting data to/from the ifreq union in a type-safe way.
ifreqData remains unexported as we can keep adding helpers similar to the
IoctlGetEthtoolDrvinfo to expose those operations in a less error-prone way.

A test is also added to verify interface index data using IoctlIfreq against
the modern rtnetlink API used by the standard library.

Change-Id: Ic6980cbcd3792cc341cd614061cce32fa1f851e7
Reviewed-on: https://go-review.googlesource.com/c/sys/+/340370
Trust: Matt Layher <mdlayher@gmail.com>
Run-TryBot: Matt Layher <mdlayher@gmail.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Ian Lance Taylor <iant@golang.org>
diff --git a/unix/ifreq_linux.go b/unix/ifreq_linux.go
index cb07859..fd3eecc 100644
--- a/unix/ifreq_linux.go
+++ b/unix/ifreq_linux.go
@@ -7,15 +7,29 @@
 
 package unix
 
-import "unsafe"
+import (
+	"bytes"
+	"unsafe"
+)
 
 // Helpers for dealing with ifreq since it contains a union and thus requires a
 // lot of unsafe.Pointer casts to use properly.
 
-// newIfreq creates an ifreq with the input network interface name after
+// An Ifreq is a type-safe wrapper around the raw ifreq struct. An Ifreq
+// contains an interface name and a union of arbitrary data which can be
+// accessed using the Ifreq's methods. To create an Ifreq, use the NewIfreq
+// function.
+//
+// Use the Name method to access the stored interface name. The union data
+// fields can be get and set using the following methods:
+//   - Uint16/SetUint16: flags
+//   - Uint32/SetUint32: ifindex, metric, mtu
+type Ifreq struct{ raw ifreq }
+
+// NewIfreq creates an Ifreq with the input network interface name after
 // validating the name does not exceed IFNAMSIZ-1 (trailing NULL required)
 // bytes.
-func newIfreq(name string) (*ifreq, error) {
+func NewIfreq(name string) (*Ifreq, error) {
 	// Leave room for terminating NULL byte.
 	if len(name) >= IFNAMSIZ {
 		return nil, EINVAL
@@ -24,25 +38,72 @@
 	var ifr ifreq
 	copy(ifr.Ifrn[:], name)
 
-	return &ifr, nil
+	return &Ifreq{raw: ifr}, nil
 }
 
-// An ifreqData is an ifreq but with a typed unsafe.Pointer field for data in
-// the union. This is required in order to comply with the unsafe.Pointer rules
-// since the "pointer-ness" of data would not be preserved if it were cast into
-// the byte array of a raw ifreq.
+// TODO(mdlayher): get/set methods for sockaddr, char array, etc.
+
+// Name returns the interface name associated with the Ifreq.
+func (ifr *Ifreq) Name() string {
+	// BytePtrToString requires a NULL terminator or the program may crash. If
+	// one is not present, just return the empty string.
+	if !bytes.Contains(ifr.raw.Ifrn[:], []byte{0x00}) {
+		return ""
+	}
+
+	return BytePtrToString(&ifr.raw.Ifrn[0])
+}
+
+// Uint16 returns the Ifreq union data as a C short/Go uint16 value.
+func (ifr *Ifreq) Uint16() uint16 {
+	return *(*uint16)(unsafe.Pointer(&ifr.raw.Ifru[:2][0]))
+}
+
+// SetUint16 sets a C short/Go uint16 value as the Ifreq's union data.
+func (ifr *Ifreq) SetUint16(v uint16) {
+	ifr.clear()
+	*(*uint16)(unsafe.Pointer(&ifr.raw.Ifru[:2][0])) = v
+}
+
+// Uint32 returns the Ifreq union data as a C int/Go uint32 value.
+func (ifr *Ifreq) Uint32() uint32 {
+	return *(*uint32)(unsafe.Pointer(&ifr.raw.Ifru[:4][0]))
+}
+
+// SetUint32 sets a C int/Go uint32 value as the Ifreq's union data.
+func (ifr *Ifreq) SetUint32(v uint32) {
+	ifr.clear()
+	*(*uint32)(unsafe.Pointer(&ifr.raw.Ifru[:4][0])) = v
+}
+
+// clear zeroes the ifreq's union field to prevent trailing garbage data from
+// being sent to the kernel if an ifreq is reused.
+func (ifr *Ifreq) clear() {
+	for i := range ifr.raw.Ifru {
+		ifr.raw.Ifru[i] = 0
+	}
+}
+
+// TODO(mdlayher): export as IfreqData? For now we can provide helpers such as
+// IoctlGetEthtoolDrvinfo which use these APIs under the hood.
+
+// An ifreqData is an Ifreq which carries pointer data. To produce an ifreqData,
+// use the Ifreq.withData method.
 type ifreqData struct {
 	name [IFNAMSIZ]byte
+	// A type separate from ifreq is required in order to comply with the
+	// unsafe.Pointer rules since the "pointer-ness" of data would not be
+	// preserved if it were cast into the byte array of a raw ifreq.
 	data unsafe.Pointer
 	// Pad to the same size as ifreq.
 	_ [len(ifreq{}.Ifru) - SizeofPtr]byte
 }
 
-// SetData produces an ifreqData with the pointer p set for ioctls which require
+// withData produces an ifreqData with the pointer p set for ioctls which require
 // arbitrary pointer data.
-func (ifr ifreq) SetData(p unsafe.Pointer) ifreqData {
+func (ifr Ifreq) withData(p unsafe.Pointer) ifreqData {
 	return ifreqData{
-		name: ifr.Ifrn,
+		name: ifr.raw.Ifrn,
 		data: p,
 	}
 }
diff --git a/unix/ifreq_linux_test.go b/unix/ifreq_linux_test.go
new file mode 100644
index 0000000..52640ac
--- /dev/null
+++ b/unix/ifreq_linux_test.go
@@ -0,0 +1,142 @@
+// Copyright 2021 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build linux
+// +build linux
+
+package unix
+
+import (
+	"testing"
+	"unsafe"
+)
+
+// An ifreqUnion is shorthand for a byte array matching the
+// architecture-dependent size of an ifreq's union field.
+type ifreqUnion = [len(ifreq{}.Ifru)]byte
+
+func TestNewIfreq(t *testing.T) {
+	// Interface name too long.
+	if _, err := NewIfreq("abcdefghijklmnop"); err != EINVAL {
+		t.Fatalf("expected error EINVAL, but got: %v", err)
+	}
+}
+
+func TestIfreqSize(t *testing.T) {
+	// Ensure ifreq (generated) and Ifreq/ifreqData (hand-written to create a
+	// safe wrapper and store a pointer field) are identical in size.
+	want := unsafe.Sizeof(ifreq{})
+	if got := unsafe.Sizeof(Ifreq{}); want != got {
+		t.Fatalf("unexpected Ifreq size: got: %d, want: %d", got, want)
+	}
+
+	if got := unsafe.Sizeof(ifreqData{}); want != got {
+		t.Fatalf("unexpected IfreqData size: got: %d, want: %d", got, want)
+	}
+}
+
+func TestIfreqName(t *testing.T) {
+	// Invalid ifreq (no NULL terminator), so expect empty string.
+	var name [IFNAMSIZ]byte
+	for i := range name {
+		name[i] = 0xff
+	}
+
+	bad := &Ifreq{raw: ifreq{Ifrn: name}}
+	if got := bad.Name(); got != "" {
+		t.Fatalf("expected empty ifreq name, but got: %q", got)
+	}
+
+	// Valid ifreq, expect the hard-coded testIfreq name.
+	ifr := testIfreq(t)
+	if want, got := ifreqName, ifr.Name(); want != got {
+		t.Fatalf("unexpected ifreq name: got: %q, want: %q", got, want)
+	}
+}
+
+func TestIfreqWithData(t *testing.T) {
+	ifr := testIfreq(t)
+
+	// Store pointer data in the ifreq so we can retrieve it and cast back later
+	// for comparison.
+	want := [5]byte{'h', 'e', 'l', 'l', 'o'}
+	ifrd := ifr.withData(unsafe.Pointer(&want[0]))
+
+	// Ensure the memory of the original Ifreq was not modified by SetData.
+	if ifr.raw.Ifru != (ifreqUnion{}) {
+		t.Fatalf("ifreq was unexpectedly modified: % #x", ifr.raw.Ifru)
+	}
+
+	got := *(*[5]byte)(ifrd.data)
+	if want != got {
+		t.Fatalf("unexpected ifreq data bytes:\n got: % #x\nwant: % #x", got, want)
+	}
+}
+
+func TestIfreqUint16(t *testing.T) {
+	ifr := testIfreq(t)
+	const in = 0x0102
+	ifr.SetUint16(in)
+
+	// The layout of the bytes depends on the machine's endianness.
+	var want ifreqUnion
+	if isBigEndian {
+		want[0] = 0x01
+		want[1] = 0x02
+	} else {
+		want[0] = 0x02
+		want[1] = 0x01
+	}
+
+	if got := ifr.raw.Ifru; want != got {
+		t.Fatalf("unexpected ifreq uint16 bytes:\n got: % #x\nwant: % #x", got, want)
+	}
+
+	if got := ifr.Uint16(); in != got {
+		t.Fatalf("unexpected ifreq uint16: got: %d, want: %d", got, in)
+	}
+}
+
+func TestIfreqUint32(t *testing.T) {
+	ifr := testIfreq(t)
+	const in = 0x01020304
+	ifr.SetUint32(in)
+
+	// The layout of the bytes depends on the machine's endianness.
+	var want ifreqUnion
+	if isBigEndian {
+		want[0] = 0x01
+		want[1] = 0x02
+		want[2] = 0x03
+		want[3] = 0x04
+	} else {
+		want[0] = 0x04
+		want[1] = 0x03
+		want[2] = 0x02
+		want[3] = 0x01
+	}
+
+	if got := ifr.raw.Ifru; want != got {
+		t.Fatalf("unexpected ifreq uint32 bytes:\n got: % #x\nwant: % #x", got, want)
+	}
+
+	if got := ifr.Uint32(); in != got {
+		t.Fatalf("unexpected ifreq uint32: got: %d, want: %d", got, in)
+	}
+}
+
+// ifreqName is a hard-coded name for testIfreq.
+const ifreqName = "eth0"
+
+// testIfreq returns an Ifreq with a populated interface name.
+func testIfreq(t *testing.T) *Ifreq {
+	t.Helper()
+
+	ifr, err := NewIfreq(ifreqName)
+	if err != nil {
+		t.Fatalf("failed to create ifreq: %v", err)
+	}
+
+	return ifr
+}
diff --git a/unix/ioctl_linux.go b/unix/ioctl_linux.go
index 013a060..1dadead 100644
--- a/unix/ioctl_linux.go
+++ b/unix/ioctl_linux.go
@@ -48,15 +48,15 @@
 // IoctlGetEthtoolDrvinfo fetches ethtool driver information for the network
 // device specified by ifname.
 func IoctlGetEthtoolDrvinfo(fd int, ifname string) (*EthtoolDrvinfo, error) {
-	ifr, err := newIfreq(ifname)
+	ifr, err := NewIfreq(ifname)
 	if err != nil {
 		return nil, err
 	}
 
 	value := EthtoolDrvinfo{Cmd: ETHTOOL_GDRVINFO}
-	ifrd := ifr.SetData(unsafe.Pointer(&value))
+	ifrd := ifr.withData(unsafe.Pointer(&value))
 
-	err = ioctlPtr(fd, SIOCETHTOOL, unsafe.Pointer(&ifrd))
+	err = ioctlIfreqData(fd, SIOCETHTOOL, &ifrd)
 	return &value, err
 }
 
@@ -176,3 +176,21 @@
 	err := ioctlPtr(fd, _HIDIOCGRAWUNIQ, unsafe.Pointer(&value[0]))
 	return ByteSliceToString(value[:]), err
 }
+
+// IoctlIfreq performs an ioctl using an Ifreq structure for input and/or
+// output. See the netdevice(7) man page for details.
+func IoctlIfreq(fd int, req uint, value *Ifreq) error {
+	// It is possible we will add more fields to *Ifreq itself later to prevent
+	// misuse, so pass the raw *ifreq directly.
+	return ioctlPtr(fd, req, unsafe.Pointer(&value.raw))
+}
+
+// TODO(mdlayher): export if and when IfreqData is exported.
+
+// ioctlIfreqData performs an ioctl using an ifreqData structure for input
+// and/or output. See the netdevice(7) man page for details.
+func ioctlIfreqData(fd int, req uint, value *ifreqData) error {
+	// The memory layout of IfreqData (type-safe) and ifreq (not type-safe) are
+	// identical so pass *IfreqData directly.
+	return ioctlPtr(fd, req, unsafe.Pointer(value))
+}
diff --git a/unix/syscall_internal_linux_test.go b/unix/syscall_internal_linux_test.go
index 9ca15fc..7ec21ca 100644
--- a/unix/syscall_internal_linux_test.go
+++ b/unix/syscall_internal_linux_test.go
@@ -14,14 +14,6 @@
 	"unsafe"
 )
 
-func Test_ifreqSize(t *testing.T) {
-	// Ensure ifreq (generated) and ifreqData (hand-written due to
-	// unsafe.Pointer field) are identical in size.
-	if want, got := unsafe.Sizeof(ifreq{}), unsafe.Sizeof(ifreqData{}); want != got {
-		t.Fatalf("unexpected ifreq size: got: %d, want: %d", got, want)
-	}
-}
-
 func makeProto(proto int) *int {
 	return &proto
 }
diff --git a/unix/syscall_linux_test.go b/unix/syscall_linux_test.go
index fe96bbe..886cd3c 100644
--- a/unix/syscall_linux_test.go
+++ b/unix/syscall_linux_test.go
@@ -125,6 +125,42 @@
 		v.Enabled, v.Time.Year+1900, v.Time.Mon+1, v.Time.Mday, v.Time.Hour, v.Time.Min, v.Time.Sec)
 }
 
+func TestIoctlIfreq(t *testing.T) {
+	s, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM, 0)
+	if err != nil {
+		t.Fatalf("failed to open socket: %v", err)
+	}
+	defer unix.Close(s)
+
+	ifis, err := net.Interfaces()
+	if err != nil {
+		t.Fatalf("failed to get network interfaces: %v", err)
+	}
+
+	// Compare the network interface fetched from rtnetlink with the data from
+	// the equivalent ioctl API.
+	for _, ifi := range ifis {
+		ifr, err := unix.NewIfreq(ifi.Name)
+		if err != nil {
+			t.Fatalf("failed to create ifreq for %q: %v", ifi.Name, err)
+		}
+
+		if err := unix.IoctlIfreq(s, unix.SIOCGIFINDEX, ifr); err != nil {
+			t.Fatalf("failed to get interface index for %q: %v", ifi.Name, err)
+		}
+
+		if want, got := ifi.Index, int(ifr.Uint32()); want != got {
+			t.Fatalf("unexpected interface index for %q: got: %d, want: %d",
+				ifi.Name, got, want)
+		}
+
+		if want, got := ifi.Name, ifr.Name(); want != got {
+			t.Fatalf("unexpected interface name for index %d: got: %q, want: %q",
+				ifi.Index, got, want)
+		}
+	}
+}
+
 func TestPpoll(t *testing.T) {
 	if runtime.GOOS == "android" {
 		t.Skip("mkfifo syscall is not available on android, skipping test")