This commit is contained in:
kayos@tcp.direct 2023-12-01 20:26:14 -08:00
commit 4e4210a43f
Signed by: kayos
GPG Key ID: 4B841471B4BEE979
9 changed files with 1352 additions and 0 deletions

62
cmd/parse/main.go Normal file

@ -0,0 +1,62 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"io"
"os"
iface "git.tcp.direct/kayos/ifupdown"
)
func main() {
eth0 := &iface.NetworkInterface{}
switch {
case len(os.Args) < 2:
buf := &bytes.Buffer{}
var empty = 0
for {
n, err := buf.ReadFrom(os.Stdin)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
panic(err)
}
if n == 0 {
empty++
}
if empty > 100 {
break
}
}
n, err := eth0.Write(buf.Bytes())
if err != nil {
panic(err)
}
if n != len(buf.Bytes()) {
panic("short write")
}
default:
dat, err := os.ReadFile(os.Args[1])
if err != nil {
panic(err)
}
n, err := eth0.Write(dat)
if err != nil {
panic(err)
}
if n != len(dat) {
panic("short write")
}
}
dat, err := json.MarshalIndent(eth0, "", "\t")
if err != nil {
panic(err)
}
if err = eth0.Validate(); err != nil {
println(err.Error())
}
_, _ = os.Stdout.Write(dat)
}

BIN
cmd/parse/parse Executable file

Binary file not shown.

19
errors.go Normal file

@ -0,0 +1,19 @@
package iface
import "errors"
var (
ErrInvalidAddress = errors.New("invalid address")
ErrInvalidMask = errors.New("invalid mask")
ErrInvalidBroadcast = errors.New("invalid broadcast")
ErrInvalidGateway = errors.New("invalid gateway")
ErrInvalidAddressVersion = errors.New("invalid address version")
ErrAddressSetWhenDHCP = errors.New("address set when DHCP enabled")
ErrAddressNotSetStatic = errors.New("address not set with static config")
ErrMaskNotSetStatic = errors.New("mask not set with static config")
ErrAdressNotLoopback = errors.New("address must be loopback when config is loopback")
ErrInterfaceHasErrors = errors.New("interface has errors")
ErrUnallocatedInterface = errors.New("unallocated interface")
ErrInvalidIfaceData = errors.New("invalid interface data provided")
ErrMultipleInterfaces = errors.New("multiple interfaces in data provided")
)

11
go.mod Normal file

@ -0,0 +1,11 @@
module git.tcp.direct/kayos/ifupdown
go 1.21.4
require (
git.tcp.direct/kayos/common v0.9.6
github.com/davecgh/go-spew v1.1.1
github.com/hashicorp/go-multierror v1.1.1
)
require github.com/hashicorp/errwrap v1.0.0 // indirect

8
go.sum Normal file

@ -0,0 +1,8 @@
git.tcp.direct/kayos/common v0.9.6 h1:EITtktxZF/zkzqAhZZxvm6cZpFYoZ0P/gLB9RPatKUY=
git.tcp.direct/kayos/common v0.9.6/go.mod h1:8y9b+PN1+ZVaQ/VugD9dkKe+uqhE8jH7a64RyF7h2rM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=

102
ifaces.go Normal file

@ -0,0 +1,102 @@
package iface
import (
"bufio"
"strings"
"github.com/hashicorp/go-multierror"
"git.tcp.direct/kayos/common/pool"
)
type poolGroup struct {
Buffers pool.BufferFactory
Strs pool.StringFactory
}
var pools = poolGroup{Buffers: pool.NewBufferFactory(), Strs: pool.NewStringFactory()}
type MultiParser struct {
Interfaces map[string]*NetworkInterface
Errs []error
buf []byte
}
func NewMultiParser() *MultiParser {
return &MultiParser{
Interfaces: make(map[string]*NetworkInterface),
Errs: make([]error, 0),
buf: make([]byte, 0),
}
}
func (p *MultiParser) Write(data []byte) (int, error) {
p.buf = append(p.buf, data...)
return len(data), nil
}
func (p *MultiParser) Parse() error {
scanner := bufio.NewScanner(strings.NewReader(string(p.buf)))
index := 0
currentIfaceName := ""
buf := pools.Buffers.Get()
defer pools.Buffers.MustPut(buf)
flush := func(name string) (*NetworkInterface, bool) {
if len(buf.Bytes()) == 0 {
return nil, false
}
defer buf.MustReset()
newIface := NewNetworkInterface(name)
if _, err := buf.WriteTo(newIface); err != nil {
p.Errs = append(p.Errs, err)
return nil, false
}
p.Interfaces[newIface.Name] = newIface
return newIface, true
}
w := func(s string) {
_, _ = buf.WriteString(s)
_ = buf.WriteByte('\n')
}
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
upForChange := currentIfaceName == "" ||
(currentIfaceName != "" && !strings.Contains(line, currentIfaceName)) ||
index == 0
startDetected := len(strings.Fields(line)) > 1 &&
(strings.HasPrefix(line, "auto") ||
strings.HasPrefix(line, "allow-") ||
strings.HasPrefix(line, "iface"))
switch {
case line == "", strings.HasPrefix(line, "#"):
continue
case upForChange && startDetected:
newName := strings.Fields(line)[1]
if ifa, ok := flush(newName); ok {
currentIfaceName = ifa.Name
index++
}
w(line)
continue
default:
w(line)
}
}
if len(buf.Bytes()) > 0 {
flush("unknown")
}
me := &multierror.Error{}
for _, err := range p.Errs {
if err != nil {
me = multierror.Append(me, err)
}
}
return me.ErrorOrNil()
}

42
ifaces_test.go Normal file

@ -0,0 +1,42 @@
package iface
import (
"testing"
)
func TestParse_SimpleValidData(t *testing.T) {
mp := NewMultiParser()
data := []byte(`# The loopback network interface
auto lo
iface lo inet loopback
# The primary network interface
allow-hotplug eth0
iface eth0 inet dhcp
`)
_, err := mp.Write(data)
if err != nil {
t.Fatalf("Write failed: %v", err)
}
err = mp.Parse()
if err != nil {
t.Fatalf("Expected nil error, got: %v", err)
}
if len(mp.Interfaces) != 2 {
t.Fatalf("Expected 2 interfaces, got: %d", len(mp.Interfaces))
}
loIface, ok := mp.Interfaces["lo"]
if !ok || loIface.Name != "lo" {
t.Fatalf("Expected to find interface 'lo', got: %+v", loIface)
}
eth0Iface, ok := mp.Interfaces["eth0"]
if !ok || eth0Iface.Name != "eth0" {
t.Fatalf("Expected to find interface 'eth0', got: %+v", eth0Iface)
}
}
// Add more test functions here to check other aspects and edge cases.

712
netif.go Normal file

@ -0,0 +1,712 @@
package iface
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net"
"net/netip"
"strings"
"sync"
)
type Hooks struct {
// PreUp of the interface
PreUp []string
// PostUp of the interface
PostUp []string
// PreDown of the interface
PreDown []string
// PostDown of the interface
PostDown []string
}
type AddressConfig uint8
const (
AddressConfigUnset AddressConfig = iota
AddressConfigLoopback
AddressConfigDHCP
AddressConfigStatic
AddressConfigManual
)
var addressConfigMap = map[AddressConfig]string{
AddressConfigLoopback: "loopback",
AddressConfigDHCP: "dhcp",
AddressConfigStatic: "static",
AddressConfigManual: "manual",
}
func (ac AddressConfig) String() string {
return addressConfigMap[ac]
}
type AddressVersion uint8
const (
AddressVersionNil AddressVersion = iota
AddressVersion4
AddressVersion6
)
func (at AddressVersion) String() string {
switch at {
case AddressVersion4:
return "inet"
case AddressVersion6:
return "inet6"
default:
}
return ""
}
// NetworkInterface follows the format of ifupdown /etc/network/interfaces
type NetworkInterface struct {
// Name of the interface.
Name string `json:"name"`
// Hotplug determines if hot plugging is allowed.
Hotplug bool `json:"hotplug,omitempty"`
// Auto determines if the interface is automatically brought up.
Auto bool `json:"auto,omitempty"`
// Address determines the static IP address of the interface.
Address net.IP `json:"address,omitempty"`
// Netmask determines the netmask of the interface.
Netmask net.IPMask `json:"netmask,omitempty"`
Broadcast net.IP `json:"broadcast,omitempty"`
Gateway net.IP `json:"gateway,omitempty"`
Config AddressConfig `json:"config,omitempty"`
Version AddressVersion `json:"version,omitempty"`
// DNSServers of the interface.
DNSServers []net.IP `json:"dns_servers,omitempty"`
// DNSSearch of the interface.
DNSSearch []string `json:"dns_search,omitempty"`
// MACAddress of the interface.
MACAddress net.HardwareAddr `json:"mac_address,omitempty"`
// Hooks contains the pre/post up/down hooks.
Hooks Hooks `json:"hooks,omitempty"`
dirty bool
allocated bool
errs []error
*sync.RWMutex
}
func NewNetworkInterface(name string) *NetworkInterface {
return &NetworkInterface{
allocated: false,
Name: name,
Auto: true,
dirty: true,
}
}
func (iface *NetworkInterface) allocate() {
if iface.RWMutex == nil {
iface.RWMutex = &sync.RWMutex{}
}
iface.Lock()
iface.dirty = true
iface.allocated = true
}
func (iface *NetworkInterface) err() error {
if len(iface.errs) == 0 {
return nil
}
err := ErrInterfaceHasErrors
for i, e := range iface.errs {
if err != nil {
if i == 0 {
err = fmt.Errorf("%w: %w", err, e)
continue
}
err = fmt.Errorf("%w, %w", err, e)
}
}
return err
}
func (iface *NetworkInterface) Validate() error {
if !iface.dirty && len(iface.errs) > 0 {
if err := iface.err(); err != nil {
return err
}
}
if iface.RWMutex == nil {
iface.RWMutex = &sync.RWMutex{}
}
iface.RLock()
defer iface.RUnlock()
iface.errs = iface.errs[:0]
if iface.allocated != true {
iface.errs = append(iface.errs, ErrUnallocatedInterface)
return iface.err()
}
if iface.Config == AddressConfigUnset {
iface.errs = append(iface.errs, fmt.Errorf("address config not set"))
}
switch iface.Config {
case AddressConfigDHCP:
if iface.Address != nil {
iface.errs = append(iface.errs, ErrAddressSetWhenDHCP)
}
case AddressConfigStatic:
switch {
case iface.Address == nil:
iface.errs = append(iface.errs, ErrAddressNotSetStatic)
case iface.Netmask == nil && iface.Version == AddressVersion4:
iface.errs = append(iface.errs, ErrMaskNotSetStatic)
case iface.Address.IsUnspecified():
iface.errs = append(iface.errs, ErrInvalidAddress)
}
case AddressConfigLoopback:
if iface.Address != nil && !iface.Address.IsLoopback() {
iface.errs = append(iface.errs, fmt.Errorf("%w: %v", ErrAdressNotLoopback, iface.Address))
}
}
switch iface.Version {
case AddressVersion4, AddressVersion6:
break
default:
iface.errs = append(iface.errs, fmt.Errorf(
"[%s] %w: %v",
iface.Name, ErrInvalidAddressVersion, iface.Version,
),
)
}
iface.dirty = false
return iface.err()
}
func (iface *NetworkInterface) WithAddress(address string) *NetworkInterface {
iface.allocate()
_, ipn, err := net.ParseCIDR(address)
if err == nil {
iface = iface.WithNetmask(ipn.Mask.Size())
}
iface.Address = net.ParseIP(address)
if iface.Address == nil {
iface.errs = append(iface.errs, fmt.Errorf("invalid address: %s", address))
iface.Unlock()
return iface
}
iface.Unlock()
return iface
}
func (iface *NetworkInterface) WithLoopback() *NetworkInterface {
iface.allocate()
iface.Config = AddressConfigLoopback
iface.Unlock()
return iface
}
func (iface *NetworkInterface) WithDHCP() *NetworkInterface {
iface.allocate()
iface.Config = AddressConfigDHCP
iface.Unlock()
return iface
}
func (iface *NetworkInterface) WithStatic() *NetworkInterface {
iface.allocate()
iface.Config = AddressConfigStatic
iface.Unlock()
return iface
}
func (iface *NetworkInterface) WithManual() *NetworkInterface {
iface.allocate()
iface.Config = AddressConfigManual
iface.Unlock()
return iface
}
func (iface *NetworkInterface) WithAddressConfig(config AddressConfig) *NetworkInterface {
iface.allocate()
iface.Config = config
iface.Unlock()
return iface
}
func (iface *NetworkInterface) WithAddressVersion(version AddressVersion) *NetworkInterface {
iface.allocate()
iface.Version = version
iface.Unlock()
return iface
}
func (iface *NetworkInterface) WithNetmask(mask, bits int) *NetworkInterface {
iface.allocate()
var parsedMask net.IPMask
if iface.Address != nil {
if iface.Address.To4() != nil || iface.Version == AddressVersion4 {
parsedMask = net.CIDRMask(mask, bits)
} else {
parsedMask = net.CIDRMask(mask, bits)
}
if parsedMask == nil {
iface.errs = append(iface.errs, fmt.Errorf("invalid mask: %d", mask))
iface.Unlock()
return iface
}
}
iface.Netmask = parsedMask
// iface.Netmask = netmask
iface.Unlock()
return iface
}
func (iface *NetworkInterface) WithBroadcast(broadcast string) *NetworkInterface {
iface.allocate()
iface.Broadcast = net.ParseIP(broadcast)
if iface.Broadcast == nil {
iface.errs = append(iface.errs, fmt.Errorf("invalid broadcast: %s", broadcast))
iface.Unlock()
return iface
}
iface.Unlock()
return iface
}
func (iface *NetworkInterface) WithGateway(gateway string) *NetworkInterface {
iface.allocate()
iface.Gateway = net.ParseIP(gateway)
if iface.Gateway == nil {
iface.errs = append(iface.errs, fmt.Errorf("invalid gateway: %s", gateway))
iface.Unlock()
return iface
}
iface.Unlock()
return iface
}
func (iface *NetworkInterface) WithConfigMethod(config AddressConfig) *NetworkInterface {
iface.allocate()
iface.Config = config
iface.Unlock()
return iface
}
func (iface *NetworkInterface) WithVersion(version AddressVersion) *NetworkInterface {
iface.allocate()
iface.Version = version
iface.Unlock()
return iface
}
func (iface *NetworkInterface) WithDNS(dnsServers []string) *NetworkInterface {
iface.allocate()
for _, dns := range dnsServers {
if nsIP := net.ParseIP(dns); nsIP != nil {
iface.DNSServers = append(iface.DNSServers, nsIP)
} else {
iface.errs = append(iface.errs, fmt.Errorf("invalid dns server: %s", dns))
}
}
iface.Unlock()
return iface
}
func (iface *NetworkInterface) WithDNSSearch(dnsSearch []string) *NetworkInterface {
iface.allocate()
iface.DNSSearch = dnsSearch
iface.Unlock()
return iface
}
func (iface *NetworkInterface) WithMACAddress(macAddress string) *NetworkInterface {
iface.allocate()
var err error
iface.MACAddress, err = net.ParseMAC(macAddress)
if err != nil {
iface.errs = append(iface.errs, fmt.Errorf("invalid mac address: %s", macAddress))
}
iface.Unlock()
return iface
}
func (iface *NetworkInterface) netMaskString(mask net.IPMask) string {
if mask == nil {
return ""
}
var m netip.Addr
if iface.Version == AddressVersion4 {
m = netip.AddrFrom4([4]byte{mask[0], mask[1], mask[2], mask[3]})
} else {
m = netip.AddrFrom16([16]byte{
mask[0], mask[1], mask[2], mask[3],
mask[4], mask[5], mask[6], mask[7],
mask[8], mask[9], mask[10], mask[11],
mask[12], mask[13], mask[14], mask[15],
})
}
if !m.IsValid() {
return ""
}
return m.String()
}
func (iface *NetworkInterface) String() string {
if iface.RWMutex == nil {
iface.RWMutex = new(sync.RWMutex)
}
iface.RLock()
defer iface.RUnlock()
if !iface.allocated {
return ""
}
if iface.Validate() != nil {
return ""
}
str := pools.Strs.Get()
defer pools.Strs.MustPut(str)
w := func(s string) {
if len(s) > 0 {
str.MustWriteString(s)
}
}
if err := iface.write(w); err != nil && !errors.Is(err, io.EOF) {
return ""
}
return str.String()
}
func (iface *NetworkInterface) Write(p []byte) (int, error) {
iface.allocate()
defer iface.Unlock()
xerox := bufio.NewScanner(bytes.NewReader(p))
numIfaces := strings.Count(string(p), "iface")
if numIfaces > 1 {
return 0, ErrMultipleInterfaces
}
for xerox.Scan() {
normalized := strings.TrimSpace(xerox.Text())
switch {
case strings.HasPrefix(normalized, "#"):
continue
case strings.HasPrefix(normalized, "auto"):
iface.Auto = true
continue
case strings.HasPrefix(normalized, "allow-hotplug"):
iface.Hotplug = true
case strings.HasPrefix(normalized, "iface"):
for i, fragment := range strings.Fields(normalized) {
// println(i, fragment)
switch i {
case 0:
if fragment != "iface" {
return 0, fmt.Errorf("%w: %s", ErrInvalidIfaceData, normalized)
}
case 1:
iface.Name = fragment
case 2:
switch fragment {
case "inet":
iface.Version = AddressVersion4
// println("version 4")
case "inet6":
iface.Version = AddressVersion6
// println("version 6")
default:
}
case 3:
switch fragment {
case "static":
iface.Config = AddressConfigStatic
case "dhcp":
iface.Config = AddressConfigDHCP
case "manual":
iface.Config = AddressConfigManual
case "loopback":
iface.Config = AddressConfigLoopback
default:
return 0, fmt.Errorf("%w: %s", ErrInvalidIfaceData, normalized)
}
default:
//
}
}
case strings.HasPrefix(normalized, "address"):
for i, fragment := range strings.Split(normalized, " ") {
switch i {
case 0:
continue
case 1:
if strings.Contains(fragment, "/") {
prfx, _ := netip.ParsePrefix(fragment)
iface.Address = net.ParseIP(prfx.Addr().String())
if iface.Address == nil {
return 0, ErrInvalidIfaceData
}
iface.Netmask = net.CIDRMask(prfx.Bits(), 8*len(iface.Address))
continue
}
iface.Address = net.ParseIP(fragment)
if iface.Address == nil {
return 0, ErrInvalidIfaceData
}
default:
//
}
}
case strings.HasPrefix(normalized, "netmask"):
if iface.Netmask != nil || iface.Version != AddressVersion4 {
continue
}
for i, fragment := range strings.Split(normalized, " ") {
switch i {
case 0:
continue
case 1:
maskBytes := net.ParseIP(fragment)
if maskBytes == nil {
return 0, ErrInvalidIfaceData
}
iface.Netmask = net.IPv4Mask(maskBytes[12], maskBytes[13], maskBytes[14], maskBytes[15])
if iface.Netmask == nil {
return 0, ErrInvalidIfaceData
}
default:
//
}
}
case strings.HasPrefix(normalized, "gateway"):
for i, fragment := range strings.Split(normalized, " ") {
switch i {
case 0:
continue
case 1:
iface.Gateway = net.ParseIP(fragment)
if iface.Gateway == nil {
return 0, ErrInvalidIfaceData
}
default:
//
}
}
case strings.HasPrefix(normalized, "dns-nameservers"):
for i, fragment := range strings.Split(normalized, " ") {
switch i {
case 0:
continue
default:
iface.DNSServers = append(iface.DNSServers, net.ParseIP(fragment))
}
}
case strings.HasPrefix(normalized, "dns-search"):
for i, fragment := range strings.Split(normalized, " ") {
switch i {
case 0:
continue
default:
iface.DNSSearch = append(iface.DNSSearch, fragment)
}
}
case strings.HasPrefix(normalized, "pre-up"):
hook := strings.TrimPrefix(normalized, "pre-up ")
if len(hook) == 0 {
continue
}
iface.Hooks.PreUp = append(iface.Hooks.PreUp, hook)
case strings.HasPrefix(normalized, "post-up"):
hook := strings.TrimPrefix(normalized, "post-up ")
if len(hook) == 0 {
continue
}
iface.Hooks.PostUp = append(iface.Hooks.PostUp, hook)
case strings.HasPrefix(normalized, "pre-down"):
hook := strings.TrimPrefix(normalized, "pre-down ")
if len(hook) == 0 {
continue
}
iface.Hooks.PreDown = append(iface.Hooks.PreDown, hook)
case strings.HasPrefix(normalized, "post-down"):
hook := strings.TrimPrefix(normalized, "post-down ")
if len(hook) == 0 {
continue
}
iface.Hooks.PostDown = append(iface.Hooks.PostDown, hook)
case strings.HasPrefix(normalized, "hwaddress"):
for i, fragment := range strings.Split(normalized, " ") {
switch i {
case 0:
continue
case 2:
var err error
iface.MACAddress, err = net.ParseMAC(fragment)
if err != nil {
return 0, ErrInvalidIfaceData
}
continue
default:
//
}
}
//
}
}
return len(p), nil
}
func (iface *NetworkInterface) Read(p []byte) (int, error) {
if err := iface.Validate(); err != nil {
return 0, err
}
var (
count = 0
wChan = make(chan string)
errChan = make(chan error)
doneChan = make(chan bool)
)
w := func(s string) {
switch {
case len(s) == 0:
return
case len(s) > len(p),
count+len(s) > len(p):
errChan <- io.ErrShortBuffer
default:
wChan <- s
}
}
go func() {
errChan <- iface.write(w)
}()
for {
select {
case <-doneChan:
return count, nil
case err := <-errChan:
if errors.Is(err, io.EOF) || err == nil {
return count, nil
}
if err != nil {
return count, err
}
case s := <-wChan:
count += copy(p[count:], s)
}
}
}
func (iface *NetworkInterface) write(w func(s string)) error {
if err := iface.Validate(); err != nil {
return err
}
if iface.Auto {
w("auto ")
w(iface.Name)
w("\n")
}
if iface.Hotplug {
w("allow-hotplug ")
w(iface.Name)
w("\n")
}
w("iface ")
w(iface.Name)
w(" ")
w(iface.Version.String())
w(" ")
w(iface.Config.String())
w("\n")
if (iface.Address != nil && iface.Netmask != nil && !iface.Address.IsUnspecified()) &&
(iface.Config == AddressConfigStatic || iface.Config == AddressConfigManual) {
w("\taddress ")
w(iface.Address.String())
w("\n")
w("\tnetmask ")
w(iface.netMaskString(iface.Netmask))
w("\n")
if iface.Broadcast != nil {
w("\tbroadcast ")
w(iface.Broadcast.String())
w("\n")
}
if iface.Gateway != nil {
w("\tgateway ")
w(iface.Gateway.String())
w("\n")
}
}
if len(iface.DNSServers) > 0 {
w("\tdns-nameservers")
for _, dns := range iface.DNSServers {
w(" ")
w(dns.String())
}
w("\n")
}
if len(iface.DNSSearch) > 0 {
w("\tdns-search")
for _, dns := range iface.DNSSearch {
w(" ")
w(dns)
}
w("\n")
}
if iface.MACAddress != nil {
w("\thwaddress ether ")
w(iface.MACAddress.String())
w("\n")
}
if len(iface.Hooks.PreUp) > 0 {
for _, hook := range iface.Hooks.PreUp {
w("\tpre-up ")
w(hook)
w("\n")
}
}
if len(iface.Hooks.PostUp) > 0 {
for _, hook := range iface.Hooks.PostUp {
w("\tpost-up ")
w(hook)
w("\n")
}
}
if len(iface.Hooks.PreDown) > 0 {
for _, hook := range iface.Hooks.PreDown {
w("\tpre-down ")
w(hook)
w("\n")
}
}
if len(iface.Hooks.PostDown) > 0 {
for _, hook := range iface.Hooks.PostDown {
w("\tpost-down ")
w(hook)
w("\n")
}
}
return io.EOF
}

396
netif_test.go Normal file

@ -0,0 +1,396 @@
package iface
import (
"bufio"
"bytes"
"errors"
"fmt"
"strings"
"testing"
"github.com/davecgh/go-spew/spew"
)
func TestAddressConfig_String(t *testing.T) {
type test struct {
name string
ac AddressConfig
want string
}
tests := []test{{
name: "static",
ac: AddressConfigStatic,
want: "static",
},
{
name: "dhcp",
ac: AddressConfigDHCP,
want: "dhcp",
},
{
name: "manual",
ac: AddressConfigManual,
want: "manual",
},
{
name: "loopback",
ac: AddressConfigLoopback,
want: "loopback",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.ac.String(); got != tt.want {
t.Errorf("String() = %v, want %v", got, tt.want)
}
})
}
}
func TestAddressType_String(t *testing.T) {
type test struct {
name string
at AddressVersion
want string
}
tests := []test{
{
name: "ipv4",
at: AddressVersion4,
want: "inet",
},
{
name: "ipv6",
at: AddressVersion6,
want: "inet6",
},
{
name: "nil",
at: AddressVersionNil,
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.at.String(); got != tt.want {
t.Errorf("String() = %v, want %v", got, tt.want)
}
})
}
}
func TestNetworkInterface_String(t *testing.T) {
type test struct {
name string
builder func() *NetworkInterface
wantErrors []error
wantString string
}
tests := []test{
{
name: "static ipv4 #1",
builder: func() *NetworkInterface {
return NewNetworkInterface("eth0").
WithStatic().
WithAddressVersion(AddressVersion4).
WithAddress("10.0.0.5").
WithNetmask(8, 32).
WithGateway("10.0.0.1")
},
wantString: `auto eth0
iface eth0 inet static
address 10.0.0.5
netmask 255.0.0.0
gateway 10.0.0.1
`,
wantErrors: nil,
},
{
name: "static ipv6 #1",
builder: func() *NetworkInterface {
return NewNetworkInterface("eth0").
WithStatic().
WithAddressVersion(AddressVersion6).
WithAddress("2001:db8::1").
WithNetmask(64, 128).
WithGateway("2001:db8::2")
},
wantString: `auto eth0
iface eth0 inet6 static
address 2001:db8::1
netmask ffff:ffff:ffff:ffff::
gateway 2001:db8::2
`,
wantErrors: nil,
},
{
name: "static ipv6 #2",
builder: func() *NetworkInterface {
return NewNetworkInterface("eth0").
WithStatic().
WithAddressVersion(AddressVersion6).
WithAddress("2001:db8::2").
WithNetmask(48, 128).
WithGateway("2001:db8::1")
},
wantString: `auto eth0
iface eth0 inet6 static
address 2001:db8::2
netmask ffff:ffff:ffff::
gateway 2001:db8::1
`,
wantErrors: nil,
},
{
name: "static ipv6 #3",
builder: func() *NetworkInterface {
return NewNetworkInterface("eth0").
WithStatic().
WithAddressVersion(AddressVersion6).
WithAddress("fc00:bbbb:bbbb:bb01::31:1927").
WithNetmask(128, 128).
WithGateway("fc00:bbbb:bbbb:bb01::1")
},
wantString: `auto eth0
iface eth0 inet6 static
address fc00:bbbb:bbbb:bb01::31:1927
netmask ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
gateway fc00:bbbb:bbbb:bb01::1
`,
wantErrors: nil,
},
{
name: "dhcp ipv4",
builder: func() *NetworkInterface {
return NewNetworkInterface("eth0").
WithDHCP().
WithAddressVersion(AddressVersion4)
},
wantString: `auto eth0
iface eth0 inet dhcp
`,
wantErrors: nil,
},
{
name: "dhcp ipv6",
builder: func() *NetworkInterface {
return NewNetworkInterface("eth0").
WithDHCP().
WithAddressVersion(AddressVersion6)
},
wantString: `auto eth0
iface eth0 inet6 dhcp
`,
wantErrors: nil,
},
{
name: "manual ipv4",
builder: func() *NetworkInterface {
return NewNetworkInterface("eth0").
WithManual().
WithAddressVersion(AddressVersion4)
},
wantString: `auto eth0
iface eth0 inet manual
`,
wantErrors: nil,
},
{
name: "manual ipv6",
builder: func() *NetworkInterface {
return NewNetworkInterface("eth0").WithManual().WithAddressVersion(AddressVersion6)
},
wantString: `auto eth0
iface eth0 inet6 manual
`,
wantErrors: nil,
},
{
name: "loopback ipv4",
builder: func() *NetworkInterface {
return NewNetworkInterface("lo").WithLoopback().WithAddressVersion(AddressVersion4)
},
wantString: `auto lo
iface lo inet loopback
`,
wantErrors: nil,
},
{
name: "loopback ipv6",
builder: func() *NetworkInterface {
return NewNetworkInterface("lo").
WithAddressConfig(AddressConfigLoopback).
WithAddressVersion(AddressVersion6)
},
wantString: `auto lo
iface lo inet6 loopback
`,
wantErrors: nil,
},
{
name: "invalid address config",
builder: func() *NetworkInterface {
return NewNetworkInterface("eth0").
WithAddress("yeeterson")
},
wantString: ``,
wantErrors: []error{fmt.Errorf(": %s", "yeeterson")},
},
{
name: "unallocated interface",
builder: func() *NetworkInterface {
return &NetworkInterface{}
},
wantString: ``,
wantErrors: []error{ErrUnallocatedInterface},
},
{
name: "invalid address version",
builder: func() *NetworkInterface {
return NewNetworkInterface("eth0").
WithAddressVersion(3)
},
wantString: ``,
wantErrors: []error{ErrInvalidAddressVersion},
},
{
name: "dirty config",
builder: func() *NetworkInterface {
ifa := NewNetworkInterface("eth0").
WithAddressVersion(3)
if ifa.Validate() == nil {
t.Errorf("invalid interface passed validation")
}
ifa = ifa.WithAddressVersion(AddressVersion4).
WithStatic().WithAddress("1.1.1.1").
WithNetmask(32, 32)
return ifa
},
wantString: `auto eth0
iface eth0 inet static
address 1.1.1.1
netmask 255.255.255.255
`,
wantErrors: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
iface := tt.builder()
if iface == nil {
t.Fatal("nil interface returned")
}
validationErrs := iface.Validate()
switch {
case len(tt.wantErrors) < 1 && validationErrs != nil:
t.Errorf("Validate(): %v", validationErrs)
spew.Dump(iface)
case len(tt.wantErrors) != 0 && len(iface.errs) == 0:
t.Errorf("interface error: %v, wanted %+v", iface.errs, tt.wantErrors)
t.Logf("%s", spew.Sdump(iface))
case len(tt.wantErrors) != 0 && len(iface.errs) != 0:
need := len(tt.wantErrors)
for _, err := range iface.errs {
if need == 0 {
t.Errorf("interface has extra error: %v", err)
continue
}
for _, wantErr := range tt.wantErrors {
if errors.Is(err, wantErr) {
need--
break
}
}
}
}
xeroxWant := bufio.NewScanner(strings.NewReader(tt.wantString))
xeroxGot := bufio.NewScanner(strings.NewReader(iface.String()))
for xeroxWant.Scan() {
if !xeroxGot.Scan() {
t.Errorf("xerox: got %s, want %s", xeroxGot.Text(), xeroxWant.Text())
continue
}
if strings.TrimSpace(xeroxGot.Text()) == "" {
xeroxGot.Scan()
}
if strings.TrimSpace(xeroxGot.Text()) != strings.TrimSpace(xeroxWant.Text()) {
t.Errorf("xerox: got %s, want %s", xeroxGot.Text(), xeroxWant.Text())
}
}
if len(tt.wantErrors) > 0 {
return
}
b := make([]byte, len(iface.String()))
n, err := iface.Read(b)
if err != nil {
t.Errorf("Read(): = %v", err)
}
if n != len(iface.String()) {
t.Errorf("Read() = %v, want %v", n, len(tt.wantString))
}
xeroxWant = bufio.NewScanner(strings.NewReader(tt.wantString))
xeroxGot = bufio.NewScanner(strings.NewReader(string(b)))
for xeroxWant.Scan() {
if !xeroxGot.Scan() {
t.Errorf("xerox: got %s, want %s", xeroxGot.Text(), xeroxWant.Text())
continue
}
if strings.TrimSpace(xeroxGot.Text()) == "" {
xeroxGot.Scan()
}
if strings.TrimSpace(xeroxGot.Text()) != strings.TrimSpace(xeroxWant.Text()) {
t.Errorf("xerox: got %s, want %s", xeroxGot.Text(), xeroxWant.Text())
}
}
newIface := &NetworkInterface{}
if n, err = newIface.Write(b); err != nil {
t.Errorf("Write(): = %v", err)
}
if n != len(b) {
t.Errorf("Write() = %v, want %v", n, len(tt.wantString))
}
newipstr, err := newIface.Address.MarshalText()
if err != nil {
t.Errorf("Write() = %v", err)
}
ipstr, err := iface.Address.MarshalText()
if err != nil {
t.Errorf("Write() = %v", err)
}
if !strings.EqualFold(string(newipstr), string(ipstr)) {
t.Errorf("Write() = %v, want %v", newipstr, ipstr)
}
if newIface.Version != iface.Version {
t.Errorf("Write() = %v, want %v", newIface.Version, iface.Version)
}
if newIface.Config != iface.Config {
t.Errorf("Write() = %v, want %v", newIface.Config, iface.Config)
}
if iface.Version == AddressVersion4 && !bytes.Equal(newIface.Netmask, iface.Netmask) {
t.Errorf("Write() netmask = %v, want %v", newIface.Netmask, iface.Netmask)
}
if newIface.Gateway.String() != iface.Gateway.String() {
t.Errorf("Write() = %v, want %v", newIface.Gateway, iface.Gateway)
}
oldIfaceIsValidated := iface.Validate() == nil
err = newIface.Validate()
if oldIfaceIsValidated && err != nil {
t.Errorf("Write() Validate = %v", err)
}
})
}
}