#!/bin/sh # # ppe_foe_dump.sh -- view & decode Airoha PPE FOE (Flow Offload Entry) # rules via devmem. # # Two access paths are used: # * SRAM portion (low indices): the PPE indirect-read window # (REG_PPE_RAM_CTRL / REG_PPE_RAM_BASE at PPE_BASE + 0x31c/0x320). # Per-PPE: hashes 0..PPE_SRAM_PER-1 live in PPE1's SRAM, the next # PPE_SRAM_PER live in PPE2's SRAM. # * DRAM extension (high indices): the table base register # REG_PPE_TB_BASE (PPE_BASE + 0x220) holds the physical address of # the DMA-coherent FOE buffer the kernel driver allocates. Hardware # populates the DRAM half via DMA, so we just devmem-read those # entries directly out of DDR. Both PPE instances share the same # DRAM buffer. # # In addition, any FOE entry whose action word has tunnel=1 references # a TUN_TBL slot; we inline-decode that slot (cfg + outer MAC/L3/L4) # so the FOE dump shows the full tunnel context. # # Read protocol per SRAM entry (mirrors airoha_ppe_foe_get_entry_locked): # 1. write CTRL = REQ | (idx << ENTRY_SHIFT) on the right PPE # 2. poll CTRL bit 31 (ACK) # 3. read RAM_ENTRY[i] for i = 0..19 -> 80 bytes # For DRAM entries we simply read 80 bytes at TB_BASE + (idx-sram)*80. # # Default target: Airoha EN7581 (FE base 0x1fb50000). EN7583 uses the # same FE base layout; override with FE_BASE for other variants. # # Usage: # ppe_foe_dump.sh # full decode of one entry (dec or 0xHEX) # ppe_foe_dump.sh # scan range, one-line per non-empty # ppe_foe_dump.sh -r # raw 20-word dump for one entry # ppe_foe_dump.sh -s # summary scan over SCAN_LIMIT entries # ppe_foe_dump.sh --scan-all # scan every SRAM+DRAM entry (slow) # ppe_foe_dump.sh --stats # histogram by state and packet-type # ppe_foe_dump.sh --geo # print detected geometry & exit # ppe_foe_dump.sh -P <0|1> # force SRAM reads through PPE1/PPE2 # # (default: auto by idx) # ppe_foe_dump.sh -T # don't inline tunnel-table data # ppe_foe_dump.sh -h | --help # # Env: # FE_BASE= override FE MMIO base (default 0x1fb50000) # PPE_SRAM_PER= per-PPE SRAM capacity (default 8192) # SRAM_TOTAL= override total SRAM entries (default: auto-detect # from REG_PPE_TB_CFG) # DRAM_ENTRIES= DRAM extension size (default 16384) # DRAM_BASE_PHYS= override DRAM-table physical base (default: read # from REG_PPE_TB_BASE) # SCAN_LIMIT= default range for summary mode (default 1024) # NO_TUNNEL=1 same as -T: skip inline tunnel-table decode # # Requires root (uses /dev/mem). Run while the kernel airoha driver is # loaded so the DRAM table base is programmed and the table is live. set -u # ------------------------------------------------------------------------ # Configuration / register layout # ------------------------------------------------------------------------ FE_BASE=${FE_BASE:-0x1fb50000} SCAN_LIMIT=${SCAN_LIMIT:-1024} # PPE1 = FE + 0x0c00, PPE2 = FE + 0x1c00 (per airoha_regs.h). PPE1_BASE_OFF=0x0c00 PPE2_BASE_OFF=0x1c00 # Within a PPE block: TB_CFG_OFF=0x21c TB_BASE_OFF=0x220 RAM_CTRL_OFF=0x31c RAM_BASE_OFF=0x320 # entry word 0; word i at base + 4*i # CTRL bits: RAM_CTRL_ACK=0x80000000 RAM_CTRL_REQ=0x1 RAM_CTRL_ENTRY_SHIFT=8 # ENTRY field at bits [23:8] # TB_CFG: SRAM entry count field at bits [26:24], DRAM at [2:0]. # Encoding: size = 1024 << field. TB_CFG_SRAM_SHIFT=24 TB_CFG_SRAM_MASK=0x7 TB_CFG_DRAM_SHIFT=0 TB_CFG_DRAM_MASK=0x7 # Entry layout: ENTRY_WORDS=20 # 80 bytes / 4 ENTRY_BYTES=80 # Poll budget for ACK. POLL_MAX=50 # Per-PPE SRAM capacity (HW constant on EN7581/7583 = 8K). PPE_SRAM_PER=${PPE_SRAM_PER:-8192} DRAM_ENTRIES=${DRAM_ENTRIES:-16384} # Auto-detected (or env override). SRAM_TOTAL=${SRAM_TOTAL:-0} DRAM_BASE_PHYS=${DRAM_BASE_PHYS:-0} # CLI options. FORCE_PPE="" # set to 0 or 1 by -P to force SRAM PPE TUNNEL_INLINE=1 # set to 0 by -T (or NO_TUNNEL=1) to skip [ "${NO_TUNNEL:-0}" = "1" ] && TUNNEL_INLINE=0 # Resolve addresses. addr_add() { printf '0x%08x\n' $(( $1 + $2 )); } PPE1_BASE=$(addr_add "$FE_BASE" "$PPE1_BASE_OFF") PPE2_BASE=$(addr_add "$FE_BASE" "$PPE2_BASE_OFF") DEVMEM=$(command -v devmem 2>/dev/null || true) require_devmem() { if [ -z "$DEVMEM" ]; then echo "error: devmem not found in PATH (need busybox devmem)" >&2 exit 1 fi if [ "$(id -u)" -ne 0 ]; then echo "warning: not running as root; devmem may fail." >&2 fi } # devmem wrappers. busybox devmem prints hex like "0x80000000". mem_read32() { "$DEVMEM" "$1" w; } mem_write32() { "$DEVMEM" "$1" w "$2" >/dev/null; } # ------------------------------------------------------------------------ # Geometry detection # ------------------------------------------------------------------------ # Read REG_PPE_TB_CFG(0) and decode the SRAM entry-count field. The # kernel programs this with __ffs(N/1024), so the live table size is # 1024 << field. Used so the script works on both EN7581 (16K total # SRAM = 8K per PPE) and EN7583 (8K total SRAM = single PPE). detect_geometry() { if [ "$SRAM_TOTAL" = "0" ]; then cfg_addr=$(addr_add "$PPE1_BASE" "$TB_CFG_OFF") cfg_val=$(mem_read32 "$cfg_addr") f=$(( (cfg_val >> TB_CFG_SRAM_SHIFT) & TB_CFG_SRAM_MASK )) SRAM_TOTAL=$(( 1024 << f )) fi if [ "$DRAM_BASE_PHYS" = "0" ]; then tb_addr=$(addr_add "$PPE1_BASE" "$TB_BASE_OFF") DRAM_BASE_PHYS=$(mem_read32 "$tb_addr") # Sanity-check: kernel sets this to a DDR pointer (top byte != 0). if [ "$(( DRAM_BASE_PHYS ))" -eq 0 ]; then echo "warning: REG_PPE_TB_BASE=0 -- driver loaded? DRAM dump unavailable." >&2 fi fi TOTAL_ENTRIES=$(( SRAM_TOTAL + DRAM_ENTRIES )) } # ------------------------------------------------------------------------ # FOE entry fetch (dispatches SRAM indirect vs. DRAM devmem) # ------------------------------------------------------------------------ # Pick the PPE base register to use for SRAM indirect access at a given # idx. Honours FORCE_PPE if set (debug override). ppe_base_for_idx() { idx=$1 if [ -n "$FORCE_PPE" ]; then [ "$FORCE_PPE" = "1" ] && { echo "$PPE2_BASE"; return; } echo "$PPE1_BASE"; return fi if [ $idx -lt $PPE_SRAM_PER ]; then echo "$PPE1_BASE" else echo "$PPE2_BASE" fi } # SRAM path: trigger a read into the indirect staging area for entry # $idx on PPE $ppe_base. Echoes 0 on success. foe_sram_request() { idx=$1; ppe_base=$2 ram_ctrl=$(addr_add "$ppe_base" "$RAM_CTRL_OFF") cfg=$(printf '0x%08x' $(( RAM_CTRL_REQ | (idx << RAM_CTRL_ENTRY_SHIFT) ))) mem_write32 "$ram_ctrl" "$cfg" i=0 while [ $i -lt $POLL_MAX ]; do v=$(mem_read32 "$ram_ctrl") if [ $(( v & RAM_CTRL_ACK )) -ne 0 ]; then return 0 fi i=$(( i + 1 )) done echo "error: PPE RAM_CTRL ACK timeout reading SRAM entry $idx" >&2 return 1 } foe_sram_read_word() { i=$1; ppe_base=$2 addr=$(addr_add "$ppe_base" "$(( RAM_BASE_OFF + i * 4 ))") mem_read32 "$addr" } # DRAM path: compute physical offset and read directly. foe_dram_read_word() { idx=$1; i=$2 phys=$(( DRAM_BASE_PHYS + (idx - SRAM_TOTAL) * ENTRY_BYTES + i * 4 )) mem_read32 "$(printf '0x%08x' $phys)" } # Echo all 20 words of entry idx as "0x.. 0x.. ... 0x..". foe_fetch_full() { idx=$1 out="" if [ $idx -lt $SRAM_TOTAL ]; then pb=$(ppe_base_for_idx $idx) foe_sram_request $idx "$pb" || return 1 i=0 while [ $i -lt $ENTRY_WORDS ]; do w=$(foe_sram_read_word $i "$pb") out="$out $w" i=$(( i + 1 )) done else if [ $idx -ge $TOTAL_ENTRIES ]; then echo "error: idx $idx out of range [0..$((TOTAL_ENTRIES - 1))]" >&2 return 1 fi if [ "$(( DRAM_BASE_PHYS ))" -eq 0 ]; then echo "error: DRAM_BASE_PHYS=0; driver not loaded?" >&2 return 1 fi i=0 while [ $i -lt $ENTRY_WORDS ]; do w=$(foe_dram_read_word $idx $i) out="$out $w" i=$(( i + 1 )) done fi echo "$out" } # Cheap path for scan/stats: fetch only word 0 (ib1). foe_fetch_ib1() { idx=$1 if [ $idx -lt $SRAM_TOTAL ]; then pb=$(ppe_base_for_idx $idx) foe_sram_request $idx "$pb" || return 1 foe_sram_read_word 0 "$pb" else if [ "$(( DRAM_BASE_PHYS ))" -eq 0 ]; then return 1; fi foe_dram_read_word $idx 0 fi } # Label which storage backs an idx; used in headers. idx_region() { idx=$1 if [ $idx -lt $SRAM_TOTAL ]; then if [ $idx -lt $PPE_SRAM_PER ]; then echo "SRAM(PPE1)" else echo "SRAM(PPE2)" fi else echo "DRAM" fi } # ------------------------------------------------------------------------ # TUN_TBL access (FE-relative; shared between PPE1 and PPE2) # ------------------------------------------------------------------------ TUN_TBL_CFG_OFF=0x207c TUN_TBL_DATA0_OFF=0x2080 TUN_TBL_DATA1_OFF=0x2084 TUN_TBL_DATA2_OFF=0x2088 TUN_TBL_DATA3_OFF=0x208c TUN_CFG_REQ=0x1 TUN_CFG_DONE=0x80000000 TUN_TBL_CFG=$(addr_add "$FE_BASE" "$TUN_TBL_CFG_OFF") TUN_TBL_DATA0=$(addr_add "$FE_BASE" "$TUN_TBL_DATA0_OFF") TUN_TBL_DATA1=$(addr_add "$FE_BASE" "$TUN_TBL_DATA1_OFF") TUN_TBL_DATA2=$(addr_add "$FE_BASE" "$TUN_TBL_DATA2_OFF") TUN_TBL_DATA3=$(addr_add "$FE_BASE" "$TUN_TBL_DATA3_OFF") # Read one (slot, offset) row of TUN_TBL. Echoes "D3 D2 D1 D0" (MSW first). tun_tbl_read_row() { slot=$1; off=$2 cfg=$(printf '0x%08x' $(( TUN_CFG_REQ \ | ( (slot & 0x3f) << 8 ) \ | ( (off & 0x7) << 16 ) ))) mem_write32 "$TUN_TBL_CFG" "$cfg" i=0 while [ $i -lt 20 ]; do v=$(mem_read32 "$TUN_TBL_CFG") if [ $(( v & TUN_CFG_DONE )) -ne 0 ]; then break; fi i=$(( i + 1 )) done if [ $i -ge 20 ]; then echo "error: TUN_TBL ACK timeout slot=$slot off=$off" >&2 return 1 fi d3=$(mem_read32 "$TUN_TBL_DATA3") d2=$(mem_read32 "$TUN_TBL_DATA2") d1=$(mem_read32 "$TUN_TBL_DATA1") d0=$(mem_read32 "$TUN_TBL_DATA0") printf '%s %s %s %s\n' "$d3" "$d2" "$d1" "$d0" } # Render a compact decode of a TUN_TBL slot, indented under the FOE # entry. Falls back gracefully if the slot reads as empty. tun_opcode_name() { case $1 in 0) echo "none" ;; 1) echo "L2" ;; 2) echo "IPv4" ;; 3) echo "IPv6" ;; 4) echo "UDP" ;; 5) echo "VXLAN" ;; 6) echo "GRE" ;; 7) echo "L2TP+PPP" ;; *) echo "rsv($1)" ;; esac } tun_kind_from_cfg() { cfg=$1 is_l2=$(( (cfg >> 31) & 0x1 )) t0=$(( (cfg >> 12) & 0xf )); t1=$(( (cfg >> 8) & 0xf )) t2=$(( (cfg >> 4) & 0xf )); t3=$(( cfg & 0xf )) for t in $t0 $t1 $t2 $t3; do case $t in 5) echo "VXLAN"; return ;; 7) echo "L2TPv2"; return ;; 6) [ $is_l2 -eq 1 ] && echo "L2GRE" || echo "L3GRE"; return ;; esac done echo "unknown" } render_tun_slot() { slot=$1 set -- $(tun_tbl_read_row $slot 4) || return 1 cfg=$4 if [ "$(( cfg ))" -eq 0 ]; then printf ' tun[%u] (cfg=0; slot empty)\n' $slot return 0 fi kind=$(tun_kind_from_cfg $cfg) t0=$(( (cfg >> 12) & 0xf )); t1=$(( (cfg >> 8) & 0xf )) t2=$(( (cfg >> 4) & 0xf )); t3=$(( cfg & 0xf )) printf ' tun[%u] kind=%s cfg=%s hops=%s/%s/%s/%s\n' \ $slot "$kind" "$cfg" \ "$(tun_opcode_name $t0)" "$(tun_opcode_name $t1)" \ "$(tun_opcode_name $t2)" "$(tun_opcode_name $t3)" # MAC row (offset 0). set -- $(tun_tbl_read_row $slot 0) || return 0 m0=$1; m1=$2; m2=$3; m3=$4 dmac_hi=$m0; dmac_lo=$(( (m1 >> 16) & 0xffff )) smac_hi=$(( m1 & 0xffff )); smac_lo=$m2 printf ' dmac=%02x:%02x:%02x:%02x:%02x:%02x smac=%02x:%02x:%02x:%02x:%02x:%02x vlan=0x%04x pppoe=0x%04x\n' \ $(( (dmac_hi >> 24) & 0xff )) $(( (dmac_hi >> 16) & 0xff )) \ $(( (dmac_hi >> 8) & 0xff )) $(( dmac_hi & 0xff )) \ $(( (dmac_lo >> 8) & 0xff )) $(( dmac_lo & 0xff )) \ $(( (smac_hi >> 8) & 0xff )) $(( smac_hi & 0xff )) \ $(( (smac_lo >> 24) & 0xff )) $(( (smac_lo >> 16) & 0xff )) \ $(( (smac_lo >> 8) & 0xff )) $(( smac_lo & 0xff )) \ $(( (m3 >> 16) & 0xffff )) $(( m3 & 0xffff )) # L3 row -- pick IPv4 vs IPv6 from cfg hops (any stage = 3 => IPv6). is_v6=0 for t in $t0 $t1 $t2 $t3; do [ $t -eq 3 ] && is_v6=1 && break; done set -- $(tun_tbl_read_row $slot 1) || return 0 ip0=$1; ip1=$2; ip2=$3; ip3=$4 if [ $is_v6 -eq 1 ]; then set -- $(tun_tbl_read_row $slot 2) || return 0 ip4=$1; ip5=$2 printf ' ipv6 hop=%d tclass=0x%02x sip_id=%d (UpdMem slot)\n' \ $(( (ip0 >> 24) & 0xff )) $(( (ip0 >> 16) & 0xff )) \ $(( ip1 & 0x7 )) printf ' dip=%x:%x:%x:%x:%x:%x:%x:%x\n' \ $(( (ip2 >> 16) & 0xffff )) $(( ip2 & 0xffff )) \ $(( (ip3 >> 16) & 0xffff )) $(( ip3 & 0xffff )) \ $(( (ip4 >> 16) & 0xffff )) $(( ip4 & 0xffff )) \ $(( (ip5 >> 16) & 0xffff )) $(( ip5 & 0xffff )) else sip=$ip2; dip=$ip3 printf ' ipv4 ttl=%d dscp=0x%02x id=0x%04x\n' \ $(( (ip0 >> 24) & 0xff )) $(( (ip0 >> 16) & 0xff )) \ $(( (ip1 >> 16) & 0xffff )) printf ' sip=%d.%d.%d.%d dip=%d.%d.%d.%d\n' \ $(( (sip >> 24) & 0xff )) $(( (sip >> 16) & 0xff )) \ $(( (sip >> 8) & 0xff )) $(( sip & 0xff )) \ $(( (dip >> 24) & 0xff )) $(( (dip >> 16) & 0xff )) \ $(( (dip >> 8) & 0xff )) $(( dip & 0xff )) fi # Proto row (off=3): l2tp, gre, vxlan, udp. set -- $(tun_tbl_read_row $slot 3) || return 0 l2tp=$1; gre=$2; vxlan=$3; udp=$4 out="" [ "$(( l2tp ))" -ne 0 ] && \ out="$out L2TP(tun=$(( (l2tp >> 16) & 0xffff )) ses=$(( l2tp & 0xffff )))" [ "$(( gre ))" -ne 0 ] && \ out="$out GRE(vsid=0x$(printf '%06x' $(( (gre >> 8) & 0xffffff ))))" [ "$(( vxlan ))" -ne 0 ] && \ out="$out VXLAN(vni=$(( (vxlan >> 8) & 0xffffff )))" [ "$(( udp ))" -ne 0 ] && \ out="$out UDP(sport=$(( (udp >> 16) & 0xffff )) dport=$(( udp & 0xffff )))" [ -n "$out" ] && printf ' %s\n' "$out" } # ------------------------------------------------------------------------ # Decoders (FOE entry words) # ------------------------------------------------------------------------ decode_ib1_bound() { v=$1 static=$(( (v >> 31) & 0x1 )) udp=$(( (v >> 30) & 0x1 )) state=$(( (v >> 28) & 0x3 )) pkttype=$(( (v >> 25) & 0x7 )) ttl=$(( (v >> 24) & 0x1 )) tnl_decap=$(( (v >> 23) & 0x1 )) pppoe=$(( (v >> 22) & 0x1 )) vpm=$(( (v >> 20) & 0x3 )) vlan_layer=$(( (v >> 16) & 0xf )) keepalive=$(( (v >> 15) & 0x1 )) timestamp=$(( v & 0x7fff )) state_str=$(state_name $state) type_str=$(pkttype_name $pkttype) udp_str="TCP"; [ $udp -eq 1 ] && udp_str="UDP" printf ' ib1[bound] state=%s type=%s udp=%s static=%d ttl_dec=%d\n' \ "$state_str" "$type_str" "$udp_str" $static $ttl printf ' tunnel_decap=%d pppoe=%d vpm=%d vlan_layer=%d keepalive=%d ts=%u\n' \ $tnl_decap $pppoe $vpm $vlan_layer $keepalive $timestamp } decode_ib1_unbound() { v=$1 state=$(( (v >> 28) & 0x3 )) prebind=$(( (v >> 24) & 0x1 )) packets=$(( (v >> 8) & 0xffff )) ts=$(( v & 0xff )) printf ' ib1[unbound] state=%s prebind=%d unbind_packets=%u ts=%u\n' \ "$(state_name $state)" $prebind $packets $ts } decode_ib1() { v=$1 state=$(( (v >> 28) & 0x3 )) if [ $state -eq 1 ]; then decode_ib1_unbound $v else decode_ib1_bound $v fi } decode_ib2() { v=$1 dscp=$(( (v >> 24) & 0xff )) port_ag=$(( (v >> 13) & 0x7ff )) pcp=$(( (v >> 12) & 0x1 )) multicast=$(( (v >> 11) & 0x1 )) fast_path=$(( (v >> 10) & 0x1 )) pse_qos=$(( (v >> 9) & 0x1 )) pse_port=$(( (v >> 5) & 0xf )) nbq=$(( v & 0x1f )) printf ' ib2 dscp=0x%02x port_ag=0x%03x pcp=%d mcast=%d fast_path=%d\n' \ $dscp $port_ag $pcp $multicast $fast_path printf ' pse_qos=%d pse_port=%s(%d) nbq=%d\n' \ $pse_qos "$(pse_port_name $pse_port)" $pse_port $nbq } # data/action word: actdp | shaper | channel | qid | dpi | tunnel | tunnel_id # Echoes the tunnel_id (decimal) on the LAST line of its stdout so the # caller can pick it up for inline TUN_TBL rendering. We use a marker # prefix on the data line for that: `TUNREF: ` (one line). decode_data() { v=$1 actdp=$(( (v >> 24) & 0xff )) shaper=$(( (v >> 16) & 0xff )) channel=$(( (v >> 11) & 0x1f )) qid=$(( (v >> 8) & 0x7 )) dpi=$(( (v >> 7) & 0x1 )) tunnel=$(( (v >> 6) & 0x1 )) tunnel_id=$(( v & 0x3f )) printf ' data actdp=0x%02x shaper_id=0x%02x channel=%d qid=%d\n' \ $actdp $shaper $channel $qid printf ' dpi=%d tunnel=%d tunnel_id=%d (TUN_TBL slot)\n' \ $dpi $tunnel $tunnel_id if [ $tunnel -eq 1 ] && [ $TUNNEL_INLINE -eq 1 ]; then render_tun_slot $tunnel_id fi } ipv4_str() { v=$1 printf '%d.%d.%d.%d' \ $(( (v >> 24) & 0xff )) $(( (v >> 16) & 0xff )) \ $(( (v >> 8) & 0xff )) $(( v & 0xff )) } decode_ipv4_tuple() { label=$1; sip=$2; dip=$3; ports=$4; pkttype=$5 printf ' %-6s sip=%s dip=%s' "$label" \ "$(ipv4_str $sip)" "$(ipv4_str $dip)" case $pkttype in 0|3) sport=$(( (ports >> 16) & 0xffff )) dport=$(( ports & 0xffff )) printf ' sport=%u dport=%u\n' $sport $dport ;; 1) proto=$(( ports & 0xff )) printf ' proto=%u (pad=0x%06x)\n' $proto $(( (ports >> 8) & 0xffffff )) ;; *) printf ' ports=0x%08x\n' $ports ;; esac } ipv6_str() { w0=$1; w1=$2; w2=$3; w3=$4 printf '%x:%x:%x:%x:%x:%x:%x:%x' \ $(( (w0 >> 16) & 0xffff )) $(( w0 & 0xffff )) \ $(( (w1 >> 16) & 0xffff )) $(( w1 & 0xffff )) \ $(( (w2 >> 16) & 0xffff )) $(( w2 & 0xffff )) \ $(( (w3 >> 16) & 0xffff )) $(( w3 & 0xffff )) } # Decode etype the way the kernel writes it. Returns a short label. # Cases: # * Standard Ethernet types (0x0800, 0x86dd, 0x8100, 0x8864, ...). # * FOE magics (FOE_MAGIC_GRE_HWDOWN_1, etc) for HW-down paths. # * DSA TX sptag: airoha_ppe.c sets `etype = BIT(dsa_port) | (vlan?0:BIT(15))` # when the egress goes through the internal switch. We map BIT(N) # back to LAN via the driver's LAN_IDX_FROM_TX_SPTAG convention. etype_decode() { v=$1 case $v in 0x0800|2048) echo "0x0800 (IPv4)"; return ;; 0x86dd|34525) echo "0x86dd (IPv6)"; return ;; 0x8100|33024) echo "0x8100 (VLAN)"; return ;; 0x88a8|34984) echo "0x88a8 (QinQ S-VLAN)"; return ;; 0x8864|34916) echo "0x8864 (PPPoE session)"; return ;; 0x8863|34915) echo "0x8863 (PPPoE discovery)"; return ;; 0x0806|2054) echo "0x0806 (ARP)"; return ;; esac # FOE magics. name=$(magic_name $v) case "$name" in 0x????" ("*) echo "$name"; return ;; esac # DSA TX sptag pattern: low bits one-hot, optional BIT(15) for no-VLAN. bit15=$(( (v >> 15) & 0x1 )) low=$(( v & 0x7fff )) if [ $low -ne 0 ] && [ $(( low & (low - 1) )) -eq 0 ]; then # one-hot: BIT(dsa_port). dsa_port 1..4 maps to LAN1..LAN4. port=0; tmp=$low while [ $tmp -gt 1 ]; do tmp=$(( tmp >> 1 )); port=$(( port + 1 )); done if [ $port -ge 1 ] && [ $port -le 4 ]; then tag="LAN${port}" [ $bit15 -eq 1 ] && tag="$tag,no-vlan" printf '0x%04x (DSA sptag → %s)' $v "$tag" return fi fi printf '0x%04x' $v } # Decode an L2 block. The Airoha FOE entry has two L2-info shapes: # # * mac_info_common (struct used by IPv6 / bridge32): the 4th word # stores `pppoe_id | smac_idx<<16 | hops<<20`. The driver writes # smac_idx through a shrink-table (UpdMem), so the actual source # MAC isn't directly in the entry -- only an index into UpdMem. # # * mac_info = mac_info_common + 2 more words (IPv4 path). Here the # driver overwrites the union with `src_mac_hi` (raw MAC bytes), so # bits that mac_info_common would read as smac_idx/hops/pppoe_id # are actually MAC bytes. The extra word holds `pppoe_id | src_mac_lo`. # # args: # has_smac_full=$1 1 if mac_info layout (IPv4 path), 0 if common-only # m0..m3=$2..$5 the 4 mac_info_common words # m4=$6 only used when has_smac_full=1: word holding # pppoe_id (low) | src_mac_lo (high) decode_mac_info() { has_smac_full=$1 m0=$2; m1=$3; m2=$4; m3=$5 m4=${6:-0} # Common pair-of-u16 fields (first field is LOW half on LE target). vlan1=$(( m0 & 0xffff )) etype=$(( (m0 >> 16) & 0xffff )) dmac_hi=$m1 vlan2=$(( m2 & 0xffff )) dmac_lo=$(( (m2 >> 16) & 0xffff )) printf ' mac dmac=%02x:%02x:%02x:%02x:%02x:%02x\n' \ $(( (dmac_hi >> 24) & 0xff )) $(( (dmac_hi >> 16) & 0xff )) \ $(( (dmac_hi >> 8) & 0xff )) $(( dmac_hi & 0xff )) \ $(( (dmac_lo >> 8) & 0xff )) $(( dmac_lo & 0xff )) if [ "$has_smac_full" = "1" ]; then # IPv4 path: w14 is src_mac_hi (4 MAC bytes), w15 carries # pppoe_id (low) and src_mac_lo (high). smac_hi=$m3 smac_lo=$(( (m4 >> 16) & 0xffff )) pppoe_id=$(( m4 & 0xffff )) printf ' smac=%02x:%02x:%02x:%02x:%02x:%02x\n' \ $(( (smac_hi >> 24) & 0xff )) $(( (smac_hi >> 16) & 0xff )) \ $(( (smac_hi >> 8) & 0xff )) $(( smac_hi & 0xff )) \ $(( (smac_lo >> 8) & 0xff )) $(( smac_lo & 0xff )) printf ' etype=%s vlan1=0x%04x vlan2=0x%04x pppoe_id=0x%04x\n' \ "$(etype_decode $etype)" $vlan1 $vlan2 $pppoe_id else # IPv6 / bridge32 path: m3 is the pppoe_id/smac_idx/hops union. pppoe_id=$(( m3 & 0xffff )) smac_idx=$(( (m3 >> 16) & 0xf )) hop3=$(( (m3 >> 20) & 0x7 )) hop2=$(( (m3 >> 23) & 0x7 )) hop1=$(( (m3 >> 26) & 0x7 )) hop0=$(( (m3 >> 29) & 0x7 )) printf ' etype=%s vlan1=0x%04x vlan2=0x%04x pppoe_id=0x%04x smac_idx=%d (UpdMem)\n' \ "$(etype_decode $etype)" $vlan1 $vlan2 $pppoe_id $smac_idx printf ' hops: hop0=%d hop1=%d hop2=%d hop3=%d\n' \ $hop0 $hop1 $hop2 $hop3 fi } # ------------------------------------------------------------------------ # Name lookups # ------------------------------------------------------------------------ state_name() { case $1 in 0) echo "INV" ;; 1) echo "UNB" ;; 2) echo "BND" ;; 3) echo "FIN" ;; esac } pkttype_name() { case $1 in 0) echo "IPv4_HNAPT" ;; 1) echo "IPv4_HNAT" ;; 2) echo "BRIDGE" ;; 3) echo "IPv4_DSLITE" ;; 4) echo "IPv6_3T" ;; 5) echo "IPv6_5T" ;; 7) echo "IPv6_6RD" ;; *) echo "UNK($1)" ;; esac } pse_port_name() { case $1 in 0) echo "CDM1" ;; 1) echo "GDM1" ;; 2) echo "GDM2" ;; 3) echo "GDM3" ;; 4) echo "PPE1" ;; 5) echo "CDM2" ;; 6) echo "CDM3" ;; 7) echo "CDM4" ;; 8) echo "PPE2" ;; 9) echo "GDM4" ;; 10) echo "CDM5" ;; 14) echo "MCST" ;; 15) echo "DROP" ;; *) echo "port$1" ;; esac } magic_name() { v=$1 case $v in 29301) echo "0x7275 (FOE_MAGIC_GE)" ;; 29306) echo "0x727a (FOE_MAGIC_GPON)" ;; 29316) echo "0x7284 (FOE_MAGIC_XSI)" ;; 29335) echo "0x7297 (FOE_MAGIC_GRE_HWDOWN_1)" ;; 29336) echo "0x7298 (FOE_MAGIC_GRE_HWDOWN_2)" ;; 29337) echo "0x7299 (FOE_MAGIC_VXLAN_HWDOWN_1)" ;; 29338) echo "0x729a (FOE_MAGIC_XSI_GDM4)" ;; 29345) echo "0x72a1 (FOE_MAGIC_IPSEC_SOE)" ;; 29346) echo "0x72a2 (FOE_MAGIC_WIREGUARD_SOE)" ;; 29440) echo "0x7300 (FOE_MAGIC_L2TP_HWDOWN_1)" ;; *) printf '0x%04x' $v ;; esac } # ------------------------------------------------------------------------ # Whole-entry rendering # ------------------------------------------------------------------------ render_full() { idx=$1; shift w0=$1; w1=$2; w2=$3; w3=$4; w4=$5; w5=$6; w6=$7; w7=$8; w8=$9 shift 9 w9=$1; w10=$2; w11=$3; w12=$4; w13=$5; w14=$6; w15=$7; w16=$8; w17=$9 shift 9 w18=$1; w19=$2 state=$(( (w0 >> 28) & 0x3 )) pkttype=$(( (w0 >> 25) & 0x7 )) printf '\n=== FOE entry %u (0x%x) -- %s ===\n' \ $idx $idx "$(idx_region $idx)" printf ' raw words:\n' j=0 for w in "$w0" "$w1" "$w2" "$w3" "$w4" "$w5" "$w6" "$w7" "$w8" "$w9" \ "$w10" "$w11" "$w12" "$w13" "$w14" "$w15" "$w16" "$w17" "$w18" "$w19"; do case $((j % 4)) in 0) printf ' +0x%02x %s' $((j * 4)) "$w" ;; *) printf ' %s' "$w" ;; esac [ $((j % 4)) -eq 3 ] && printf '\n' j=$((j + 1)) done [ $((j % 4)) -ne 0 ] && printf '\n' if [ $state -eq 0 ]; then printf ' (state=INVALID; entry is empty)\n' return 0 fi printf ' decoded:\n' decode_ib1 $w0 case $pkttype in 0|1|3) decode_ib2 $w4 decode_ipv4_tuple "orig" $w1 $w2 $w3 $pkttype [ $pkttype -ne 1 ] && \ decode_ipv4_tuple "new" $w5 $w6 $w7 $pkttype decode_data $w10 # IPv4 uses the full mac_info (smac in w14 + w15 high half). decode_mac_info 1 $w11 $w12 $w13 $w14 $w15 ;; 4|5|7) decode_ib2 $w11 printf ' sip6=%s\n' "$(ipv6_str $w1 $w2 $w3 $w4)" printf ' dip6=%s\n' "$(ipv6_str $w5 $w6 $w7 $w8)" if [ $pkttype -eq 5 ]; then printf ' ports sport=%u dport=%u\n' \ $(( (w9 >> 16) & 0xffff )) $(( w9 & 0xffff )) fi decode_data $w10 # IPv6 uses mac_info_common (smac via UpdMem shrink table). decode_mac_info 0 $w12 $w13 $w14 $w15 ;; 2) decode_ib2 $w11 decode_data $w10 # bridge32 has its own sptag field at w5's high half; the # mac_info is mac_info_common (w12..w15). printf ' L2B in_vlan=0x%04x sptag=0x%04x\n' \ $(( (w2 >> 16) & 0xffff )) $(( (w5 >> 16) & 0xffff )) decode_mac_info 0 $w12 $w13 $w14 $w15 ;; *) printf ' (unknown packet_type=%d; skipping type-specific decode)\n' $pkttype ;; esac } render_summary_line() { idx=$1; shift w0=$1; w4=$5; w10=${10}; w11=${11} state=$(( (w0 >> 28) & 0x3 )) [ $state -eq 0 ] && return 1 pkttype=$(( (w0 >> 25) & 0x7 )) if [ $pkttype -ge 4 ]; then ib2=$w11; else ib2=$w4; fi pse_port=$(( (ib2 >> 5) & 0xf )) data=$w10 tunnel_id=$(( data & 0x3f )) tunnel=$(( (data >> 6) & 0x1 )) reg=$(idx_region $idx) printf ' %5u %-10s %s %-11s port=%-4s tunnel=%d(slot=%d) ib1=%s\n' \ $idx "$reg" "$(state_name $state)" "$(pkttype_name $pkttype)" \ "$(pse_port_name $pse_port)" $tunnel $tunnel_id "$w0" return 0 } # ------------------------------------------------------------------------ # Top-level operations # ------------------------------------------------------------------------ print_header() { printf 'Airoha PPE FOE dump (FE_BASE=%s)\n' "$FE_BASE" printf ' PPE1=%s PPE2=%s\n' "$PPE1_BASE" "$PPE2_BASE" printf ' SRAM_TOTAL=%u (per-PPE=%u) DRAM_ENTRIES=%u DRAM_BASE_PHYS=%s\n' \ $SRAM_TOTAL $PPE_SRAM_PER $DRAM_ENTRIES "$DRAM_BASE_PHYS" printf ' total indices: 0..%u (SRAM 0..%u, DRAM %u..%u)\n' \ $((TOTAL_ENTRIES - 1)) $((SRAM_TOTAL - 1)) \ $SRAM_TOTAL $((TOTAL_ENTRIES - 1)) if [ "${FORCE_PPE:-}" != "" ]; then printf ' forcing SRAM access through PPE%d\n' $((FORCE_PPE + 1)) fi if [ $TUNNEL_INLINE -eq 0 ]; then printf ' tunnel-table inlining: OFF\n' fi } cmd_one() { idx=$1 require_devmem detect_geometry print_header words=$(foe_fetch_full $idx) || return 1 render_full $idx $words } cmd_range() { lo=$1; hi=$2 require_devmem detect_geometry print_header printf ' idx region state type port tunnel(slot) ib1\n' printf ' ----- ---------- ---- ----------- ---- ------------------ ----------\n' n_seen=0; n_shown=0; n_tun=0 i=$lo while [ $i -le $hi ]; do words=$(foe_fetch_full $i 2>/dev/null) || { i=$((i + 1)); continue; } n_seen=$((n_seen + 1)) if render_summary_line $i $words; then n_shown=$((n_shown + 1)) # Re-extract w10 to count tunnel-bound rows. set -- $words w10=${10} [ $(( (w10 >> 6) & 0x1 )) -eq 1 ] && n_tun=$((n_tun + 1)) fi i=$((i + 1)) done printf '\n scanned %d entries, %d non-empty (%d tunnel-bound)\n' \ $n_seen $n_shown $n_tun } cmd_raw() { idx=$1 require_devmem detect_geometry print_header words=$(foe_fetch_full $idx) || return 1 printf '\nEntry %u raw words (%s):\n' $idx "$(idx_region $idx)" set -- $words i=0 while [ $i -lt $ENTRY_WORDS ]; do eval "w=\${$((i + 1))}" printf ' +0x%02x w%-2d %s\n' $((i * 4)) $i "$w" i=$((i + 1)) done } cmd_stats() { require_devmem detect_geometry print_header # Honour SCAN_LIMIT default = 1024, but allow the user to push it # higher with the env var if they want a full-table histogram. upper=$SCAN_LIMIT [ $upper -gt $TOTAL_ENTRIES ] && upper=$TOTAL_ENTRIES printf '\nScanning entries 0..%u (SCAN_LIMIT=%u)...\n\n' \ $((upper - 1)) $SCAN_LIMIT bnd=0; unb=0; fin=0; inv=0 t_hnapt=0; t_hnat=0; t_brg=0; t_dslite=0 t_v6_3t=0; t_v6_5t=0; t_6rd=0; t_other=0 n_tun=0 i=0 while [ $i -lt $upper ]; do ib1=$(foe_fetch_ib1 $i 2>/dev/null) || { i=$((i + 1)); continue; } state=$(( (ib1 >> 28) & 0x3 )) case $state in 0) inv=$((inv + 1)); i=$((i + 1)); continue ;; 1) unb=$((unb + 1)) ;; 2) bnd=$((bnd + 1)) ;; 3) fin=$((fin + 1)) ;; esac pt=$(( (ib1 >> 25) & 0x7 )) case $pt in 0) t_hnapt=$((t_hnapt + 1)) ;; 1) t_hnat=$((t_hnat + 1)) ;; 2) t_brg=$((t_brg + 1)) ;; 3) t_dslite=$((t_dslite + 1)) ;; 4) t_v6_3t=$((t_v6_3t + 1)) ;; 5) t_v6_5t=$((t_v6_5t + 1)) ;; 7) t_6rd=$((t_6rd + 1)) ;; *) t_other=$((t_other + 1)) ;; esac i=$((i + 1)) done printf 'By state:\n' printf ' BND %5d UNB %5d FIN %5d INV %5d\n' $bnd $unb $fin $inv printf 'By packet-type (non-INVALID only):\n' printf ' IPv4_HNAPT %5d\n' $t_hnapt printf ' IPv4_HNAT %5d\n' $t_hnat printf ' IPv4_DSLITE %5d\n' $t_dslite printf ' IPv6_3T %5d\n' $t_v6_3t printf ' IPv6_5T %5d\n' $t_v6_5t printf ' IPv6_6RD %5d\n' $t_6rd printf ' BRIDGE %5d\n' $t_brg printf ' other %5d\n' $t_other } cmd_geo() { require_devmem detect_geometry print_header } # Debug helper: render a TUN_TBL slot directly. Mirrors what # decode_data() does when a FOE entry has tunnel=1, but without needing # a live tunnel-bound flow. cmd_tun() { slot=$1 require_devmem detect_geometry print_header printf '\nTUN_TBL slot %u decoded:\n' $slot render_tun_slot $slot } # ------------------------------------------------------------------------ # Argument parser # ------------------------------------------------------------------------ usage() { sed -n '2,60p' "$0" | sed -n 's/^# \{0,1\}//p' >&2 } main() { mode=""; arg1=""; arg2=""; scan_all=0 while [ $# -gt 0 ]; do case "$1" in -h|--help) usage; exit 0 ;; -s|--summary) mode="range"; arg1=0; arg2=$((SCAN_LIMIT - 1)) ;; --scan-all) mode="range"; scan_all=1 ;; -r|--raw) mode="raw"; arg1=${2:?-r requires }; shift ;; --stats) mode="stats" ;; --geo|--geometry) mode="geo" ;; --tun) mode="tun"; arg1=${2:?--tun requires }; shift ;; -T|--no-tunnel) TUNNEL_INLINE=0 ;; -P|--ppe) FORCE_PPE=${2:?-P requires 0 or 1} case "$FORCE_PPE" in 0|1) ;; *) echo "error: -P expects 0 or 1" >&2; exit 2 ;; esac shift ;; --) shift; break ;; -*) echo "unknown option: $1" >&2; usage; exit 2 ;; *) if [ -z "$arg1" ]; then arg1=$1; mode="one" elif [ -z "$arg2" ]; then arg2=$1; mode="range" else usage; exit 2 fi ;; esac shift done if [ -z "$mode" ]; then usage; exit 0; fi normalise() { case "$1" in 0x*|0X*) printf '%d' "$1" ;; *) printf '%s' "$1" ;; esac } [ -n "$arg1" ] && arg1=$(normalise "$arg1") [ -n "$arg2" ] && arg2=$(normalise "$arg2") # --scan-all needs the detected geometry; defer to cmd_range and let # it pin the upper bound after detect_geometry runs. if [ "$mode" = "range" ] && [ $scan_all -eq 1 ]; then require_devmem detect_geometry arg1=0 arg2=$((TOTAL_ENTRIES - 1)) fi case "$mode" in one) cmd_one "$arg1" ;; range) cmd_range "$arg1" "$arg2" ;; raw) cmd_raw "$arg1" ;; stats) cmd_stats ;; geo) cmd_geo ;; tun) cmd_tun "$arg1" ;; esac } main "$@"