Skip to content

Support direct net.*, net/netip.* types #426

@nf-brentsaner

Description

@nf-brentsaner

It would be a huge burden lift if one was able to parse stdlib net.* and net/netip.* types as arguments without needing to either create a wrapper flags.Unmarshaler type every single time or by setting them as string arguments and explicitly parsing them.

I'd imagine they're quite common arguments.

PoC/test case under the fold.

main.go

package main

import (
	"errors"
	"fmt"
	"log"
	"math/bits"
	"net"
	"net/netip"
	"strconv"
	"strings"

	"github.com/davecgh/go-spew/spew"
	"github.com/jessevdk/go-flags"
)

type (
	Args struct {
		IP   net.IP     `short:"i" long:"ip" default:"127.0.0.1" description:"net.IP"`
		Addr netip.Addr `short:"a" long:"addr" default:"127.0.0.1" description:"net/netip.Addr"`
		// This needs some special syntax logic. By default, CIDR -- as show below as the default value.
		// However, an alternate form `<IP>_<MASK>` should be handled, where `<MASK>` is equivalent to the
		// Args.Mask syntax rules; e.g. `127.0.0.0_255.255.255.0`
		// See parseNet for an example.
		IPNet net.IPNet `short:"n" long:"ipnet" default:"127.0.0.0/8" description:"net.IPNet"`
		// Same syntax rules as Args.IPNet.
		// See parsePrefix for an example.
		Pfx      netip.Prefix   `short:"p" long:"prefix" default:"127.0.0.0/8" description:"net/netip.Prefix"`
		IPAddr   *net.IPAddr    `short:"A" long:"ipaddr" default:"fe80::5c9a:9aff:fec3:e2f%eth0" description:"net.IPAddr"`
		AddrPort netip.AddrPort `short:"P" long:"addrport" default:"[::1]:80" description:"net/netip.AddrPort"`
		// This needs some special syntax logic. These forms should be supported (where N is a number):
		// `N.N.N.N` - This would be parsed as an IPv4 netmask in traditional "dotted-quad" form. Exclusive to IPv4.
		// `4:N` - This would be an explicit mask *integer*. Exclusive to IPv4.
		/*		(There is no "uint128", so it's impossible to represent IPv6 CIDR as a mask integer
				without a math.BigInt or some other custom encapsulation type.)
				e.g. an IPv4 /24 would be represented as `4:4294967040` because `uint32(0xffffffff) << uint32(32 - uint8(<CIDR>))`
					(0xffffffff == 4294967295 == (1<<32 - 1) == math.MaxUint32, take your pick.)
		*/
		// `/N` - This would be parsed as an IPv6 CIDR.
		// `6/N` - This would also be parsed as an IPv6 CIDR.
		// `4/N` - This would be parsed as an IPv4 CIDR.
		// See parseMask for an example.
		Mask net.IPMask `short:"m" long:"mask" default:"255.255.255.0" description:"net.IPMask"`
		// This needs some special syntax logic. By default, interface name -- as shown below as the default value.
		// However, an alternate form `idx:<IDX>` should be handled, where `<IDX>` is an interface index.
		// e.g. `idx:1`
		// See parseIface for an example.
		Iface *net.Interface `short:"I" long:"device" default:"lo" description:"net.Interface"`
	}
)

var (
	ErrBadAddr  error = errors.New("invalid IP address")
	ErrBadIface error = errors.New("invalid interface")
	ErrBadMask  error = errors.New("invalid netmask")
)

// parseIface is a quick example of parsing a passed Args.Iface value.
func parseIface(s string) (iface *net.Interface, err error) {

	var ifaceIdx int
	var sl []string

	sl = strings.Split(s, ":")
	switch len(sl) {
	case 2:
		// idx:<IDX>... maybe.
		if sl[0] != "idx" {
			err = ErrBadIface
			return
		}
		// OK it is.
		if ifaceIdx, err = strconv.Atoi(sl[1]); err != nil {
			log.Panicln(err)
		}
		if iface, err = net.InterfaceByIndex(ifaceIdx); err != nil {
			return
		}
	case 1:
		// <interface name>
		if iface, err = net.InterfaceByName(s); err != nil {
			return
		}
	default:
		err = ErrBadIface
		return
	}

	return
}

// parseMask is a quick example of parsing a passed Args.Mask value.
func parseMask(s string) (mask net.IPMask, err error) {

	var b []byte
	var u64 uint64
	var bitLen int
	var sl []string

	sl = strings.Split(s, ".")
	switch len(sl) {
	case 4:
		// "Dotted-quad". Conveniently, e.g. 255.255.255.0 is a net.IPMask([]byte{255, 255, 255, 0})
		b = make([]byte, 4)
		for idx, oct := range sl {
			if u64, err = strconv.ParseUint(oct, 10, 8); err != nil {
				return
			}
			b[idx] = uint8(u64)
		}
		mask = net.IPMask(b)
		// Alternatively:
		// mask = net.IPv4Mask(b[0], b[1], b[2], b[3])
	case 1:
		// It's not dotted-quad, so it's another form.
		sl = strings.Split(s, ":")
		switch len(sl) {
		case 2:
			// Explicit mask integer... maybe.
			if sl[0] != "4" {
				err = ErrBadMask
				return
			}
			// OK it is.
			if u64, err = strconv.ParseUint(sl[1], 10, 32); err != nil {
				return
			}
			mask = net.CIDRMask(bits.LeadingZeros32(uint32(u64)), 32)
		case 1:
			// It's not the explicit mask integer either.
			// Try the CIDR format.
			sl = strings.Split(s, "/")
			switch len(sl) {
			case 2:
				switch sl[0] {
				case "", "6":
					// IPv6
					bitLen = 128
				case "4":
					// IPv4
					bitLen = 32
				default:
					err = ErrBadMask
					return
				}
				if u64, err = strconv.ParseUint(sl[1], 10, 8); err != nil {
					return
				}
				mask = net.CIDRMask(int(u64), bitLen)
			default:
				// All syntaxes have been exhausted.
				err = ErrBadMask
				return
			}
		default:
			err = ErrBadMask
			return
		}
	default:
		err = ErrBadMask
		return
	}

	return
}

// parseNet is a quick example of passing a parsed Args.IPNet value.
func parseNet(s string) (ipnet *net.IPNet, err error) {

	var sl []string
	var ip net.IP
	var mask net.IPMask

	sl = strings.SplitN(s, "_", 2)
	switch len(sl) {
	case 2:
		// <IP>_<MASK>
		if ip = net.ParseIP(sl[0]); ip == nil {
			err = ErrBadAddr
			return
		}
		if mask, err = parseMask(sl[1]); err != nil {
			return
		}
		ipnet = &net.IPNet{
			IP:   ip,
			Mask: mask,
		}
	case 1:
		// <IP>/<CIDR>
		if _, ipnet, err = net.ParseCIDR(s); err != nil {
			return
		}
	}

	return
}

// parsePrefix is a quick example of parsing a passed Args.Pfx value.
func parsePrefix(s string) (pfx netip.Prefix, err error) {

	var cidr int
	var sl []string
	var addr netip.Addr
	var mask net.IPMask

	sl = strings.SplitN(s, "_", 2)
	switch len(sl) {
	case 2:
		// <IP>_<MASK>
		if addr, err = netip.ParseAddr(sl[0]); err != nil {
			return
		}
		if mask, err = parseMask(sl[1]); err != nil {
			return
		}
		cidr, _ = mask.Size()
		pfx = netip.PrefixFrom(addr, cidr)
	case 1:
		// <IP>/<CIDR>
		if pfx, err = netip.ParsePrefix(s); err != nil {
			return
		}
	}

	return
}

func main() {
	var err error
	var args *Args = new(Args)
	var flagsErr *flags.Error = new(flags.Error)
	var parser *flags.Parser = flags.NewParser(args, flags.Default)

	_, err = parser.Parse()

	// spew.Dump(parser)

	spew.Dump(args)

	if err != nil {
		switch {
		case errors.As(err, &flagsErr):
			switch {
			case errors.Is(flagsErr.Type, flags.ErrHelp),
				errors.Is(flagsErr.Type, flags.ErrCommandRequired),
				errors.Is(flagsErr.Type, flags.ErrRequired):
				return
			default:
				log.Panicln(err)
			}
		default:
			log.Panicln(err)
		}
	}

	fmt.Println("Done!")
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions