// dominantField looks through the fields, all of which are known to
// have the same name, to find the single field that dominates the
// others using Go's embedding rules, modified by the presence of
// JSON tags. If there are multiple top-level fields, the boolean
// will be false: This condition is an error in Go and we skip all
// the fields.
func dominantField(fields []field) (field, bool) {
	// The fields are sorted in increasing index-length order. The winner
	// must therefore be one with the shortest index length. Drop all
	// longer entries, which is easy: just truncate the slice.
	length := len(fields[0].index)
	tagged := -1 // Index of first tagged field.
	for i, f := range fields {
		if len(f.index) > length {
			fields = fields[:i]
			break
		}
		if f.tag {
			if tagged >= 0 {
				// Multiple tagged fields at the same level: conflict.
				// Return no field.
				return field{}, false
			}
			tagged = i
		}
	}
	if tagged >= 0 {
		return fields[tagged], true
	}
	// All remaining fields have the same length. If there's more than one,
	// we have a conflict (two fields named "X" at the same level) and we
	// return no field.
	if len(fields) > 1 {
		return field{}, false
	}
	return fields[0], true
}

var fieldCache struct {
	sync.RWMutex
	m map[reflect.Type][]field
}

// cachedTypeFields is like typeFields but uses a cache to avoid repeated work.
func cachedTypeFields(t reflect.Type) []field {
	fieldCache.RLock()
	f := fieldCache.m[t]
	fieldCache.RUnlock()
	if f != nil {
		return f
	}

	// Compute fields without lock.
	// Might duplicate effort but won't hold other computations back.
	f = typeFields(t)
	if f == nil {
		f = []field{}
	}

	fieldCache.Lock()
	if fieldCache.m == nil {
		fieldCache.m = map[reflect.Type][]field{}
	}
	fieldCache.m[t] = f
	fieldCache.Unlock()
	return f
}

func isValidTag(s string) bool {
	if s == "" {
		return false
	}
	for _, c := range s {
		switch {
		case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c):
			// Backslash and quote chars are reserved, but
			// otherwise any punctuation chars are allowed
			// in a tag name.
		default:
			if !unicode.IsLetter(c) && !unicode.IsDigit(c) {
				return false
			}
		}
	}
	return true
}

const (
	caseMask     = ^byte(0x20) // Mask to ignore case in ASCII.
	kelvin       = ''
	smallLongEss = 'ſ'
)

// foldFunc returns one of four different case folding equivalence
// functions, from most general (and slow) to fastest:
//
// 1) bytes.EqualFold, if the key s contains any non-ASCII UTF-8
// 2) equalFoldRight, if s contains special folding ASCII ('k', 'K', 's', 'S')
// 3) asciiEqualFold, no special, but includes non-letters (including _)
// 4) simpleLetterEqualFold, no specials, no non-letters.
//
// The letters S and K are special because they map to 3 runes, not just 2:
//   - S maps to s and to U+017F 'ſ' Latin small letter long s
//   - k maps to K and to U+212A 'K' Kelvin sign
//
// See http://play.golang.org/p/tTxjOc0OGo
//
// The returned function is specialized for matching against s and
// should only be given s. It's not curried for performance reasons.
func foldFunc(s []byte) func(s, t []byte) bool {
	nonLetter := false
	special := false // special letter
	for _, b := range s {
		if b >= utf8.RuneSelf {
			return bytes.EqualFold
		}
		upper := b & caseMask
		if upper < 'A' || upper > 'Z' {
			nonLetter = true
		} else if upper == 'K' || upper == 'S' {
			// See above for why these letters are special.
			special = true
		}
	}
	if special {
		return equalFoldRight
	}
	if nonLetter {
		return asciiEqualFold
	}
	return simpleLetterEqualFold
}

// equalFoldRight is a specialization of bytes.EqualFold when s is
// known to be all ASCII (including punctuation), but contains an 's',
// 'S', 'k', or 'K', requiring a Unicode fold on the bytes in t.
// See comments on foldFunc.
func equalFoldRight(s, t []byte) bool {
	for _, sb := range s {
		if len(t) == 0 {
			return false
		}
		tb := t[0]
		if tb < utf8.RuneSelf {
			if sb != tb {
				sbUpper := sb & caseMask
				if 'A' <= sbUpper && sbUpper <= 'Z' {
					if sbUpper != tb&caseMask {
						return false
					}
				} else {
					return false
				}
			}
			t = t[1:]
			continue
		}
		// sb is ASCII and t is not. t must be either kelvin
		// sign or long s; sb must be s, S, k, or K.
		tr, size := utf8.DecodeRune(t)
		switch sb {
		case 's', 'S':
			if tr != smallLongEss {
				return false
			}
		case 'k', 'K':
			if tr != kelvin {
				return false
			}
		default:
			return false
		}
		t = t[size:]

	}

	return len(t) <= 0
}

// asciiEqualFold is a specialization of bytes.EqualFold for use when
// s is all ASCII (but may contain non-letters) and contains no
// special-folding letters.
// See comments on foldFunc.
func asciiEqualFold(s, t []byte) bool {
	if len(s) != len(t) {
		return false
	}
	for i, sb := range s {
		tb := t[i]
		if sb == tb {
			continue
		}
		if ('a' <= sb && sb <= 'z') || ('A' <= sb && sb <= 'Z') {
			if sb&caseMask != tb&caseMask {
				return false
			}
		} else {
			return false
		}
	}
	return true
}

// simpleLetterEqualFold is a specialization of bytes.EqualFold for
// use when s is all ASCII letters (no underscores, etc) and also
// doesn't contain 'k', 'K', 's', or 'S'.
// See comments on foldFunc.
func simpleLetterEqualFold(s, t []byte) bool {
	if len(s) != len(t) {
		return false
	}
	for i, b := range s {
		if b&caseMask != t[i]&caseMask {
			return false
		}
	}
	return true
}

// tagOptions is the string following a comma in a struct field's "json"
// tag, or the empty string. It does not include the leading comma.
type tagOptions string

// parseTag splits a struct field's json tag into its name and
// comma-separated options.
func parseTag(tag string) (string, tagOptions) {
	if idx := strings.Index(tag, ","); idx != -1 {
		return tag[:idx], tagOptions(tag[idx+1:])
	}
	return tag, tagOptions("")
}

// Contains reports whether a comma-separated list of options
// contains a particular substr flag. substr must be surrounded by a
// string boundary or commas.
func (o tagOptions) Contains(optionName string) bool {
	if len(o) == 0 {
		return false
	}
	s := string(o)
	for s != "" {
		var next string
		i := strings.Index(s, ",")
		if i >= 0 {
			s, next = s[:i], s[i+1:]
		}
		if s == optionName {
			return true
		}
		s = next
	}
	return false
}



package kyaml

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"regexp"
	"strconv"
	"strings"
	"time"
	"unicode"
	"unicode/utf8"

	yaml "go.yaml.in/yaml/v3"
)

// Encoder formats objects or YAML data (JSON is valid YAML) into KYAML. KYAML
// is halfway between YAML and JSON, but is a strict subset of YAML, so it
// should should be readable by any YAML parser. It is designed to be explicit
// and unambiguous, and eschews significant whitespace.
type Encoder struct {
	// Compact tells the encoder to use compact formatting. This puts all the
	// data on one line, with no extra newlines, no comments, and no multi-line
	// formatting.
	Compact bool
}

// FromYAML renders a KYAML (multi-)document from YAML bytes (JSON is YAML),
// including the KYAML header. The result always has a trailing newline.
func (ky *Encoder) FromYAML(in io.Reader, out io.Writer) error {
	// We need a YAML decoder to handle multi-document streams.
	dec := yaml.NewDecoder(in)

	// Process each document in the stream.
	for {
		var doc yaml.Node
		err := dec.Decode(&doc)
		if err == io.EOF {
			break
		}
		if err != nil {
			return fmt.Errorf("error decoding: %v", err)
		}
		if doc.Kind != yaml.DocumentNode {
			return fmt.Errorf("kyaml internal error: line %d: expected a document node, got %s", doc.Line, ky.nodeKindString(doc.Kind))
		}

		// Always emit a document separator, which helps disambiguate between YAML
		// and JSON.
		if _, err := fmt.Fprintln(out, "---"); err != nil {
			return err
		}

		if err := ky.renderDocument(&doc, 0, ky.flags(), out); err != nil {
			return err
		}
		fmt.Fprintf(out, "
")
	}

	return nil
}

// FromObject renders a KYAML document from a Go object, including the KYAML
// header. The result always has a trailing newline.
func (ky *Encoder) FromObject(obj any, out io.Writer) error {
	jb, err := json.Marshal(obj)
	if err != nil {
		return fmt.Errorf("error marshaling to JSON: %v", err)
	}
	// JSON is YAML.
	return ky.FromYAML(bytes.NewReader(jb), out)
}

// Marshal renders a single Go object as KYAML, without the header or trailing
// newline.
func (ky *Encoder) Marshal(obj any) ([]byte, error) {
	// Convert the object to JSON bytes to take advantage of all the JSON tag
	// handling and things like that.
	jb, err := json.Marshal(obj)
	if err != nil {
		return nil, fmt.Errorf("error marshaling to JSON: %v", err)
	}

	buf := &bytes.Buffer{}
	// JSON is YAML.
	if err := ky.fromObjectYAML(bytes.NewReader(jb), buf); err != nil {
		return nil, fmt.Errorf("error rendering object: %v", err)
	}
	return buf.Bytes(), nil
}

func (ky *Encoder) fromObjectYAML(in io.Reader, out io.Writer) error {
	yb, err := io.ReadAll(in)
	if err != nil {
		return err
	}

	var doc yaml.Node
	if err := yaml.Unmarshal(yb, &doc); err != nil {
		return fmt.Errorf("error decoding: %v", err)
	}
	if doc.Kind != yaml.DocumentNode {
		return fmt.Errorf("kyaml internal error: line %d: expected document node, got %s", doc.Line, ky.nodeKindString(doc.Kind))
	}

	if err := ky.renderNode(&doc, 0, ky.flags(), out); err != nil {
		return fmt.Errorf("error rendering document: %v", err)
	}

	return nil
}

// From the YAML spec.
const (
	intTag       = "!!int"
	floatTag     = "!!float"
	boolTag      = "!!bool"
	strTag       = "!!str"
	timestampTag = "!!timestamp"
	seqTag       = "!!seq"
	mapTag       = "!!map"
	nullTag      = "!!null"
	binaryTag    = "!!binary"
	mergeTag     = "!!merge"
)

type flagMask uint64

const (
	flagsNone     flagMask = 0
	flagLazyQuote flagMask = 0x01
	flagCompact   flagMask = 0x02
)

// flags returns a flagMask representing the current encoding options. It can
// be used directly or OR'ed with another mask.
func (ky *Encoder) flags() flagMask {
	flags := flagsNone
	if ky.Compact {
		flags |= flagCompact
	}
	return flags
}

// renderNode processes a YAML node, calling the appropriate render function
// for its type.  Each render function should assume that the output "cursor"
// is positioned at the start of the node and should not emit a final newline.
// If a render function needs to linewrap or indent (e.g. a struct), it should
// assume the indent level is currently correct for the node type itself, and
// may need to indent more.
func (ky *Encoder) renderNode(node *yaml.Node, indent int, flags flagMask, out io.Writer) error {
	if node == nil {
		return nil
	}

	switch node.Kind {
	case yaml.DocumentNode:
		return ky.renderDocument(node, indent, flags, out)
	case yaml.ScalarNode:
		return ky.renderScalar(node, indent, flags, out)
	case yaml.SequenceNode:
		return ky.renderSequence(node, indent, flags, out)
	case yaml.MappingNode:
		return ky.renderMapping(node, indent, flags, out)
	case yaml.AliasNode:
		return ky.renderAlias(node, indent, flags, out)
	}
	return fmt.Errorf("kyaml internal error: line %d: unknown node kind %v", node.Line, node.Kind)
}

// renderDocument processes a YAML document node, rendering it to the output.
// This function assumes that the output "cursor" is positioned at the start of
// the document. This does not emit a final newline.
func (ky *Encoder) renderDocument(doc *yaml.Node, indent int, flags flagMask, out io.Writer) error {
	if len(doc.Content) == 0 {
		return fmt.Errorf("kyaml internal error: line %d: document has no content node (%d)", doc.Line, len(doc.Content))
	}
	if len(doc.Content) > 1 {
		return fmt.Errorf("kyaml internal error: line %d: document has more than one content node (%d)", doc.Line, len(doc.Content))
	}
	if indent != 0 {
		return fmt.Errorf("kyaml internal error: line %d: document non-zero indent (%d)", doc.Line, indent)
	}

	compact := flags&flagCompact != 0

	// For document nodes, the cursor is assumed to be ready to render.
	child := doc.Content[0]
	if !compact {
		if len(doc.HeadComment) > 0 {
			ky.renderComments(doc.HeadComment, indent, out)
			fmt.Fprint(out, "
")
		}
		if len(child.HeadComment) > 0 {
			ky.renderComments(child.HeadComment, indent, out)
			fmt.Fprint(out, "
")
		}
	}
	if err := ky.renderNode(child, indent, flags, out); err != nil {
		return err
	}
	if !compact {
		if len(child.LineComment) > 0 {
			ky.renderComments(" "+child.LineComment, 0, out)
		}
		if len(child.FootComment) > 0 {
			fmt.Fprint(out, "
")
			ky.renderComments(child.FootComment, indent, out)
		}
		if len(doc.LineComment) > 0 {
			fmt.Fprint(out, "
")
			ky.renderComments(" "+doc.LineComment, 0, out)
		}
		if len(doc.FootComment) > 0 {
			fmt.Fprint(out, "
")
			ky.renderComments(doc.FootComment, indent, out)
		}
	}
	return nil
}

// renderScalar processes a YAML scalar node, rendering it to the output.  This
// DOES NOT render a trailing newline or head/line/foot comments, as those
// require the parent context.
func (ky *Encoder) renderScalar(node *yaml.Node, indent int, flags flagMask, out io.Writer) error {
	switch node.Tag {
	case intTag, floatTag, boolTag, nullTag:
		fmt.Fprint(out, node.Value)
	case strTag, timestampTag:
		return ky.renderString(node.Value, indent+1, flags, out)
	default:
		return fmt.Errorf("kyaml internal error: line %d: unknown tag %q on scalar node %q", node.Line, node.Tag, node.Value)
	}
	return nil
}

const kyamlFoldStr = "\
"

var regularEscapeMap = map[rune]string{
	'
': "\n" + kyamlFoldStr, // use YAML's line folding to make the output more readable
	'	': "	",                 // literal tab
}
var compactEscapeMap = map[rune]string{
	'
': "\n",
	'	': "\t",
}
Back to Blog

Linux Networking Troubleshooting

Michael Duren
Linux Networking Troubleshooting
#Linux#CLI#DevOps

WARNING: this article is WIP

This assumes you have the nmcli installed.

1. Identify the Ethernet interface

nmcli device status

What it does: Lists every network interface NetworkManager knows about — WiFi, Ethernet, bridges, VPNs, etc. — along with its type and connection state.

Why we ran it: Needed to know the exact device name of the physical Ethernet port plugged into the switch, since Linux doesn’t always call it something intuitive like eth0. In this case it was enp3s0u2u2u3. This naming convention is Linux’s “predictable network interface names” scheme, encoding bus/slot/port info — that’s why it looks cryptic compared to old-style eth0.


2. Create a static IP connection profile

nmcli connection add type ethernet ifname enp3s0u2u2u3 con-name udm-recovery ip4 192.168.1.25/24 gw4 192.168.1.1

What it does: Creates a saved NetworkManager connection profile (not yet active) called udm-recovery, bound specifically to the enp3s0u2u2u3 interface.

Why we specified a manual IP instead of using DHCP: A device in Recovery Mode doesn’t run a DHCP server — it’s a minimal bootloader-level environment, not the full router OS. Normally when you plug into a router/UDM, it hands your laptop an IP automatically via DHCP. In recovery mode nothing is offering that, so your laptop has no way to get an address on its own. We had to manually put the laptop on the same subnet (192.168.1.x) as the UDM’s known recovery-mode fallback address, or there’d be no way for the two devices to route traffic to each other at all — like trying to mail a letter with no return address in the same zip code.

Breaking down the flags:

  • type ethernet — this is a wired connection profile.
  • ifname enp3s0u2u2u3 — bind it to that specific physical interface.
  • con-name udm-recovery — just a friendly label so we can reference/delete it later.
  • ip4 192.168.1.25/24 — manually assigns the laptop the address 192.168.1.25, with a /24 (i.e. 255.255.255.0) subnet mask. The specific number .25 was arbitrary — any unused address in 192.168.1.2192.168.1.254 would’ve worked. It only matters that it’s in the same subnet as the device we’re trying to reach.
  • gw4 192.168.1.1 — sets a default gateway. (This turned out to be the wrong address for where the UDM was actually listening — see step 4.)

Why the specific IP “doesn’t matter” here: This was a closed, point-to-point network — just the laptop and the UDM on an isolated switch segment, nothing else handing out or claiming addresses. In a normal home/office network you’d have to pick an IP carefully to avoid colliding with other devices or the DHCP pool. Here, since nothing else was contending for addresses, almost any valid address in the subnet would have worked identically.


3. Activate the profile

nmcli connection up udm-recovery

What it does: Switches the enp3s0u2u2u3 interface over to actually use the udm-recovery profile — this is the step that actually applies the static IP to the interface (step 2 only created the profile, it didn’t apply it).


4. Test connectivity

ping 192.168.1.1

Result: Failed — Destination Host Unreachable. This specific error (not a timeout) means the laptop couldn’t even get an ARP response, i.e. nothing on the local network claimed that address.

ping 192.168.1.30

(after recreating the profile with the corrected gateway — see step 5)

Result: Succeeded. This confirmed the UDM’s recovery mode was actually listening on 192.168.1.30, not the more commonly referenced 192.168.1.1. Ubiquiti’s docs list .1 as the primary fallback and .30 as secondary — this device happened to be on the secondary.


5. Delete and recreate the profile with the correct gateway

nmcli connection delete udm-recovery
nmcli connection add type ethernet ifname enp3s0u2u2u3 con-name udm-recovery ip4 192.168.1.25/24 gw4 192.168.1.30
nmcli connection up udm-recovery

What it does: Removes the old profile (which pointed at .1) and recreates it identically except pointing the gateway at .30, then reactivates it.

Why recreate instead of edit: Could have used nmcli connection modify to just change the gateway field, but delete-and-recreate is simpler to reason about when troubleshooting quickly — guarantees no leftover stale settings from the first attempt.


6. Root cause of the initial failure (not a command, but important)

The .30 IP was actually correct the whole time — the real problem was that the laptop was physically plugged into the wrong Ethernet port (a dock/hub port not actually bridged to the switch). Once moved to the correct port, .30 connected immediately. This is a good reminder: when ping gives “Destination Host Unreachable” rather than a timeout, it’s often a link-layer problem (wrong port, bad cable, switch config) rather than an IP/subnet mismatch.


7. Access the recovery web UI

http://192.168.1.30

Opened in a browser once ping succeeded — this loaded the UDM’s Recovery Mode interface, where we ran a filesystem check and uploaded firmware to reflash it.


8. After successful recovery — clean up the static profile

nmcli connection delete udm-recovery

What it does: Removes the manual static-IP profile entirely.

Why: Once the UDM finishes rebooting after a factory reset, it comes back up as a normal router again — meaning it now runs its own DHCP server and expects to be the one handing out addresses (typically starting from 192.168.1.1 as its own address). Keeping a static 192.168.1.25 profile around would fight with that DHCP server and cause IP conflicts, so we deleted it and let NetworkManager go back to normal automatic DHCP behavior on that interface.


Quick command cheat-sheet

Command Purpose
nmcli device status List interfaces, find the Ethernet device name
nmcli connection add type ethernet ifname <if> con-name <name> ip4 <ip>/<mask> gw4 <gw> Create a static IP profile
nmcli connection up <name> Activate a saved profile
nmcli connection down <name> Deactivate a profile without deleting it
nmcli connection delete <name> Remove a saved profile entirely
ip addr show <if> Confirm what IP is actually applied to an interface
ping <ip> Test basic Layer 3 reachability