diff --git a/unix/ifreq_linux.go b/unix/ifreq_linux.go index cb07859f..fd3eeccc 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 @@ func newIfreq(name string) (*ifreq, error) { 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 00000000..52640ac1 --- /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 013a0606..1dadead2 100644 --- a/unix/ioctl_linux.go +++ b/unix/ioctl_linux.go @@ -48,15 +48,15 @@ func IoctlSetRTCWkAlrm(fd int, value *RTCWkAlrm) error { // 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 @@ func IoctlHIDGetRawUniq(fd int) (string, error) { 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 9ca15fcd..7ec21ca7 100644 --- a/unix/syscall_internal_linux_test.go +++ b/unix/syscall_internal_linux_test.go @@ -14,14 +14,6 @@ import ( "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 fe96bbee..886cd3c1 100644 --- a/unix/syscall_linux_test.go +++ b/unix/syscall_linux_test.go @@ -125,6 +125,42 @@ func TestIoctlGetRTCWkAlrm(t *testing.T) { 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")