RFC Errata
RFC 4890, "Recommendations for Filtering ICMPv6 Messages in Firewalls", May 2007
Source of RFC: v6ops (ops)
Errata ID: 7922
Status: Held for Document Update
Type: Technical
Publication Format(s) : TEXT
Reported By: William N.
Date Reported: 2024-05-03
Held for Document Update by: Mohamed Boucadair
Date Held: 2025-06-02
Section Appendix B says:
#!/bin/bash
# Set of prefixes on the trusted ("inner") side of the firewall
export INNER_PREFIXES="2001:DB8:85::/60"
# Set of hosts providing services so that they can be made pingable
export PINGABLE_HOSTS="2001:DB8:85::/64"
# Configuration option: Change this to 1 if errors allowed only for
# existing sessions
export STATE_ENABLED=0
# Configuration option: Change this to 1 if messages to/from link
# local addresses should be filtered.
# Do not use this if the firewall is a bridge.
# Optional for firewalls that are routers.
export FILTER_LINK_LOCAL_ADDRS=0
# Configuration option: Change this to 0 if the site does not support
# Mobile IPv6 Home Agents - see Appendix A.14
export HOME_AGENTS_PRESENT=1
# Configuration option: Change this to 0 if the site does not support
# Mobile IPv6 mobile nodes being present on the site -
# see Appendix A.14
export MOBILE_NODES_PRESENT=1
ip6tables -N icmpv6-filter
ip6tables -A FORWARD -p icmpv6 -j icmpv6-filter
# Match scope of src and dest else deny
# This capability is not provided for in base ip6tables functionality
# An extension (agr) exists which may support it.
#@TODO@
# ECHO REQUESTS AND RESPONSES
# ===========================
# Allow outbound echo requests from prefixes which belong to the site
for inner_prefix in $INNER_PREFIXES
do
ip6tables -A icmpv6-filter -p icmpv6 -s $inner_prefix \
--icmpv6-type echo-request -j ACCEPT
done
# Allow inbound echo requests towards only predetermined hosts
for pingable_host in $PINGABLE_HOSTS
do
ip6tables -A icmpv6-filter -p icmpv6 -d $pingable_host \
--icmpv6-type echo-request -j ACCEPT
done
if [ "$STATE_ENABLED" -eq "1" ]
then
# Allow incoming and outgoing echo reply messages
# only for existing sessions
ip6tables -A icmpv6-filter -m state -p icmpv6 \
--state ESTABLISHED,RELATED --icmpv6-type \
echo-reply -j ACCEPT
else
# Allow both incoming and outgoing echo replies
for pingable_host in $PINGABLE_HOSTS
do
# Outgoing echo replies from pingable hosts
ip6tables -A icmpv6-filter -p icmpv6 -s $pingable_host \
--icmpv6-type echo-reply -j ACCEPT
done
# Incoming echo replies to prefixes which belong to the site
for inner_prefix in $INNER_PREFIXES
do
ip6tables -A icmpv6-filter -p icmpv6 -d $inner_prefix \
--icmpv6-type echo-reply -j ACCEPT
done
fi
# Deny icmps to/from link local addresses
# If the firewall is a router:
# These rules should be redundant as routers should not forward
# link local addresses but to be sure...
# DO NOT ENABLE these rules if the firewall is a bridge
if [ "$FILTER_LINK_LOCAL_ADDRS" -eq "1" ]
then
ip6tables -A icmpv6-filter -p icmpv6 -d fe80::/10 -j DROP
ip6tables -A icmpv6-filter -p icmpv6 -s fe80::/10 -j DROP
fi
# Drop echo replies which have a multicast address as a
# destination
ip6tables -A icmpv6-filter -p icmpv6 -d ff00::/8 \
--icmpv6-type echo-reply -j DROP
# DESTINATION UNREACHABLE ERROR MESSAGES
# ======================================
if [ "$STATE_ENABLED" -eq "1" ]
then
# Allow incoming destination unreachable messages
# only for existing sessions
for inner_prefix in $INNER_PREFIXES
do
ip6tables -A icmpv6-filter -m state -p icmpv6 \
-d $inner_prefix \
--state ESTABLISHED,RELATED --icmpv6-type \
destination-unreachable -j ACCEPT
done
else
# Allow incoming destination unreachable messages
for inner_prefix in $INNER_PREFIXES
do
ip6tables -A icmpv6-filter -p icmpv6 -d $inner_prefix \
--icmpv6-type destination-unreachable -j ACCEPT
done
fi
# Allow outgoing destination unreachable messages
for inner_prefix in $INNER_PREFIXES
do
ip6tables -A icmpv6-filter -p icmpv6 -s $inner_prefix \
--icmpv6-type destination-unreachable -j ACCEPT
done
# PACKET TOO BIG ERROR MESSAGES
# =============================
if [ "$STATE_ENABLED" -eq "1" ]
then
# Allow incoming Packet Too Big messages
# only for existing sessions
for inner_prefix in $INNER_PREFIXES
do
ip6tables -A icmpv6-filter -m state -p icmpv6 \
-d $inner_prefix \
--state ESTABLISHED,RELATED \
--icmpv6-type packet-too-big \
-j ACCEPT
done
else
# Allow incoming Packet Too Big messages
for inner_prefix in $INNER_PREFIXES
do
ip6tables -A icmpv6-filter -p icmpv6 -d $inner_prefix \
--icmpv6-type packet-too-big -j ACCEPT
done
fi
# Allow outgoing Packet Too Big messages
for inner_prefix in $INNER_PREFIXES
do
ip6tables -A icmpv6-filter -p icmpv6 -s $inner_prefix \
--icmpv6-type packet-too-big -j ACCEPT
done
# TIME EXCEEDED ERROR MESSAGES
# ============================
if [ "$STATE_ENABLED" -eq "1" ]
then
# Allow incoming time exceeded code 0 messages
# only for existing sessions
for inner_prefix in $INNER_PREFIXES
do
ip6tables -A icmpv6-filter -m state -p icmpv6 \
-d $inner_prefix \
--state ESTABLISHED,RELATED --icmpv6-type packet-too-big \
-j ACCEPT
done
else
# Allow incoming time exceeded code 0 messages
for inner_prefix in $INNER_PREFIXES
do
ip6tables -A icmpv6-filter -p icmpv6 -d $inner_prefix \
--icmpv6-type ttl-zero-during-transit -j ACCEPT
done
fi
#@POLICY@
# Allow incoming time exceeded code 1 messages
for inner_prefix in $INNER_PREFIXES
do
ip6tables -A icmpv6-filter -p icmpv6 -d $inner_prefix \
--icmpv6-type ttl-zero-during-reassembly -j ACCEPT
done
# Allow outgoing time exceeded code 0 messages
for inner_prefix in $INNER_PREFIXES
do
ip6tables -A icmpv6-filter -p icmpv6 -s $inner_prefix \
--icmpv6-type ttl-zero-during-transit -j ACCEPT
done
#@POLICY@
# Allow outgoing time exceeded code 1 messages
for inner_prefix in $INNER_PREFIXES
do
ip6tables -A icmpv6-filter -p icmpv6 -s $inner_prefix \
--icmpv6-type ttl-zero-during-reassembly -j ACCEPT
done
# PARAMETER PROBLEM ERROR MESSAGES
# ================================
if [ "$STATE_ENABLED" -eq "1" ]
then
# Allow incoming parameter problem code 1 and 2 messages
# for an existing session
for inner_prefix in $INNER_PREFIXES
do
ip6tables -A icmpv6-filter -m state -p icmpv6 \
-d $inner_prefix \
--state ESTABLISHED,RELATED --icmpv6-type \
unknown-header-type \
-j ACCEPT
ip6tables -A icmpv6-filter -m state -p icmpv6 \
-d $inner_prefix \
--state ESTABLISHED,RELATED \
--icmpv6-type unknown-option \
-j ACCEPT
done
fi
# Allow outgoing parameter problem code 1 and code 2 messages
for inner_prefix in $INNER_PREFIXES
do
ip6tables -A icmpv6-filter -p icmpv6 -s $inner_prefix \
--icmpv6-type unknown-header-type -j ACCEPT
ip6tables -A icmpv6-filter -p icmpv6 -s $inner_prefix \
--icmpv6-type unknown-option -j ACCEPT
done
#@POLICY@
# Allow incoming and outgoing parameter
# problem code 0 messages
for inner_prefix in $INNER_PREFIXES
do
ip6tables -A icmpv6-filter -p icmpv6 \
--icmpv6-type bad-header \
-j ACCEPT
done
# NEIGHBOR DISCOVERY MESSAGES
# ===========================
# Drop NS/NA messages both incoming and outgoing
ip6tables -A icmpv6-filter -p icmpv6 \
--icmpv6-type neighbor-solicitation -j DROP
ip6tables -A icmpv6-filter -p icmpv6 \
--icmpv6-type neighbor-advertisement -j DROP
# Drop RS/RA messages both incoming and outgoing
ip6tables -A icmpv6-filter -p icmpv6 \
--icmpv6-type router-solicitation -j DROP
ip6tables -A icmpv6-filter -p icmpv6 \
--icmpv6-type router-advertisement -j DROP
# Drop Redirect messages both incoming and outgoing
ip6tables -A icmpv6-filter -p icmpv6 --icmpv6-type redirect -j DROP
# MLD MESSAGES
# ============
# Drop incoming and outgoing
# Multicast Listener queries (MLDv1 and MLDv2)
ip6tables -A icmpv6-filter -p icmpv6 --icmpv6-type 130 -j DROP
# Drop incoming and outgoing Multicast Listener reports (MLDv1)
ip6tables -A icmpv6-filter -p icmpv6 --icmpv6-type 131 -j DROP
# Drop incoming and outgoing Multicast Listener Done messages (MLDv1)
ip6tables -A icmpv6-filter -p icmpv6 --icmpv6-type 132 -j DROP
# Drop incoming and outgoing Multicast Listener reports (MLDv2)
ip6tables -A icmpv6-filter -p icmpv6 --icmpv6-type 143 -j DROP
# ROUTER RENUMBERING MESSAGES
# ===========================
# Drop router renumbering messages
ip6tables -A icmpv6-filter -p icmpv6 --icmpv6-type 138 -j DROP
# NODE INFORMATION QUERIES
# ========================
# Drop node information queries (139) and replies (140)
ip6tables -A icmpv6-filter -p icmpv6 --icmpv6-type 139 -j DROP
ip6tables -A icmpv6-filter -p icmpv6 --icmpv6-type 140 -j DROP
# MOBILE IPv6 MESSAGES
# ====================
# If there are mobile ipv6 home agents present on the
# trusted side allow
if [ "$HOME_AGENTS_PRESENT" -eq "1" ]
then
for inner_prefix in $INNER_PREFIXES
do
#incoming Home Agent address discovery request
ip6tables -A icmpv6-filter -p icmpv6 -d $inner_prefix \
--icmpv6-type 144 -j ACCEPT
#outgoing Home Agent address discovery reply
ip6tables -A icmpv6-filter -p icmpv6 -s $inner_prefix \
--icmpv6-type 145 -j ACCEPT
#incoming Mobile prefix solicitation
ip6tables -A icmpv6-filter -p icmpv6 -d $inner_prefix \
--icmpv6-type 146 -j ACCEPT
#outgoing Mobile prefix advertisement
ip6tables -A icmpv6-filter -p icmpv6 -s $inner_prefix \
--icmpv6-type 147 -j ACCEPT
done
fi
# If there are roaming mobile nodes present on the
# trusted side allow
if [ "$MOBILE_NODES_PRESENT" -eq "1" ]
then
for inner_prefix in $INNER_PREFIXES
do
#outgoing Home Agent address discovery request
ip6tables -A icmpv6-filter -p icmpv6 -s $inner_prefix \
--icmpv6-type 144 -j ACCEPT
#incoming Home Agent address discovery reply
ip6tables -A icmpv6-filter -p icmpv6 -d $inner_prefix \
--icmpv6-type 145 -j ACCEPT
#outgoing Mobile prefix solicitation
ip6tables -A icmpv6-filter -p icmpv6 -s $inner_prefix \
--icmpv6-type 146 -j ACCEPT
#incoming Mobile prefix advertisement
ip6tables -A icmpv6-filter -p icmpv6 -d $inner_prefix \
--icmpv6-type 147 -j ACCEPT
done
fi
# DROP EVERYTHING ELSE
# ====================
ip6tables -A icmpv6-filter -p icmpv6 -j DROP
It should say:
#!/bin/bash
# Prefixes on the trusted ("inner") side of the firewall
readonly inner_prefixes=( "2001:db8:85::/60" )
# Hosts providing services so that they can be made pingable
readonly pingable_hosts=( "2001:db8:85::/64" )
# 1 - if errors are allowed only for existing sessions
readonly state_enabled=0
# 1 - if messages to/from link local addresses should be filtered
# Do not use this if the firewall is a bridge.
# Optional for firewalls that are routers.
readonly filter_linklocal_addrs=0
# 0 - if the site does not support Mobile IPv6 Home Agents
# 1 - if there are mobile ipv6 home agents present on the trusted side
# see Appendix A.14
readonly home_agents_present=1
# 0 - if the site does not support Mobile IPv6 mobile nodes
# 1 - if there are roaming mobile nodes present on the trusted side
# see Appendix A.14
readonly mobile_nodes_present=1
ip6tables -N icmpv6-filter
ip6tables -A FORWARD -p icmpv6 -j icmpv6-filter
state_args=(
'--match' 'state'
'--state' 'ESTABLISHED,RELATED'
)
(( state_enabled == 0 )) && state_args=()
readonly state_args
# Helper functions
filter() { ip6tables -A icmpv6-filter -p icmpv6 "${@}"; }
accept() { filter "${@}" -j ACCEPT; }
drop() { filter "${@}" -j DROP; }
# Match scope of src and dest else deny
# This capability is not provided for in base ip6tables functionality
# An extension (agr) exists which may support it.
#@TODO@
# ECHO REQUESTS AND RESPONSES
# ===========================
# Outbound echo requests from prefixes belonging to the site
for inner_prefix in "${inner_prefixes[@]}"; do
accept \
-s "${inner_prefix}" \
--icmpv6-type echo-request
done
# Inbound echo requests only towards predetermined hosts
for pingable_host in "${pingable_hosts[@]}"; do
accept \
-d "${pingable_host}" \
--icmpv6-type echo-request
done
if (( state_enabled == 1 )); then
# Incoming and outgoing messages
# only for existing sessions
accept \
"${state_args[@]}" \
--icmpv6-type echo-reply
else
# Both incoming and outgoing echo replies
for pingable_host in "${pingable_hosts[@]}"; do
# Outgoing echo replies from pingable hosts
accept \
-s "${pingable_host}" \
--icmpv6-type echo-reply
done
# Incoming echo replies to prefixes belonging to the site
for inner_prefix in "${inner_prefixes[@]}"; do
accept \
-d "${inner_prefix}" \
--icmpv6-type echo-reply
done
fi
# Deny icmps to/from link local addresses
# If the firewall is a router:
# These rules should be redundant as routers should not forward
# link local addresses but to be sure...
# DO NOT ENABLE these rules if the firewall is a bridge
if (( filter_linklocal_addrs == 1 )); then
drop -d fe80::/10
drop -s fe80::/10
fi
# No echo replies for multicast destination addresses
drop \
-d ff00::/8 \
--icmpv6-type echo-reply
for inner_prefix in "${inner_prefixes[@]}"; do
# DESTINATION UNREACHABLE ERROR MESSAGES
# ======================================
# incoming
accept \
-d "${inner_prefix}" \
"${state_args[@]}" \
--icmpv6-type destination-unreachable
# outgoing
accept \
-s "${inner_prefix}" \
--icmpv6-type destination-unreachable
# PACKET TOO BIG ERROR MESSAGES
# =============================
# incoming
accept \
-d "${inner_prefix}" \
"${state_args[@]}" \
--icmpv6-type packet-too-big
# outgoing
accept \
-s "${inner_prefix}" \
--icmpv6-type packet-too-big
# TIME EXCEEDED ERROR MESSAGES
# ============================
# incoming w/ code 0
accept \
-d "${inner_prefix}" \
"${state_args[@]}" \
--icmpv6-type 3/0
# @POLICY@
# incoming w/ code 1
accept \
-d "${inner_prefix}" \
--icmpv6-type 3/1
# outgoing w/ code 0
accept \
-s "${inner_prefix}" \
--icmpv6-type 3/0
# @POLICY@
# outgoing w/ code 1
accept \
-s "${inner_prefix}" \
--icmpv6-type 3/1
# PARAMETER PROBLEM ERROR MESSAGES
# ================================
if (( state_enabled == 1 )); then
# incoming
accept \
-d "${inner_prefix}" \
"${state_args[@]}" \
--icmpv6-type 4/1
accept \
-d "${inner_prefix}" \
"${state_args[@]}" \
--icmpv6-type 4/2
fi
# outgoing
accept \
-s "${inner_prefix}" \
--icmpv6-type 4/1
accept \
-s "${inner_prefix}" \
--icmpv6-type 4/2
# @POLICY@
# incoming and outgoing
accept --icmpv6-type 4/0
done
# Drop all these, both incoming and outgoing
types=(
# NEIGHBOR DISCOVERY MESSAGES
# ===========================
'135/0' # Neighbor solicitation
'136/0' # Neighbor advertisement
'133/0' # Router solicitation
'134/0' # Router advertisement
'137/0' # Rredirect'
# Multicast Listener Discovery messages
# =====================================
130 # ML queries (MLDv1 and MLDv2)
131 # ML reports (MLDv1)
132 # ML Done messages (MLDv1)
143 # ML reports (MLDv2)
138 # Router renumbering messages
# NODE INFORMATION QUERIES
# ========================
139 # Node information queries
140 # Node information replies
)
for type in "${types[@]}"; do
drop --icmpv6-type "${type}"
done
# MOBILE IPv6 MESSAGES
# ====================
for inner_prefix in "${inner_prefixes[@]}"; do
if (( home_agents_present == 1 )); then
# incoming Home Agent address discovery request
accept \
-d "${inner_prefix}" \
--icmpv6-type 144
# outgoing Home Agent address discovery reply
accept \
-s "${inner_prefix}" \
--icmpv6-type 145
# incoming Mobile prefix solicitation
accept \
-d "${inner_prefix}" \
--icmpv6-type 146
# outgoing Mobile prefix advertisement
accept \
-s "${inner_prefix}" \
--icmpv6-type 147
fi
if (( mobile_nodes_present == 1 )); then
# outgoing Home Agent address discovery request
accept \
-s "${inner_prefix}" \
--icmpv6-type 144
# incoming Home Agent address discovery reply
accept \
-d "${inner_prefix}" \
--icmpv6-type 145
# outgoing Mobile prefix solicitation
accept \
-s "${inner_prefix}" \
--icmpv6-type 146
# incoming Mobile prefix advertisement
accept \
-d "${inner_prefix}" \
--icmpv6-type 147
fi
done
# DROP EVERYTHING ELSE
# ====================
drop
Notes:
- Fix ShellCheck SC2086 warnings
- Code formatting: improve redability
- Remove unnecessary export statements
- Make uppercase variables lowercase constants
- Make pingable_hosts and inner_prefixes arrays, so that for loops make sense
- Use lowercase IPv6 addresses, as commonly accepted
- Remove useless newlines at the beginning of if-checks and for-loops
- Reduce code repetition by using state_args array
- Combine separate loops in sections
-- DESTINATION UNREACHABLE ERROR MESSAGES
-- PACKET TOO BIG ERROR MESSAGES
-- TIME EXCEEDED ERROR MESSAGES
-- PARAMETER PROBLEM ERROR MESSAGES
- Create filter, accept and drop functions to simplify code
- Reduce code repetition by using type array
- Simplify comments. Comments should explain why something is done, not what is done. If code needs explanation about what it does, it is not readable
NOTE: The original does not seem to address type codes as advised and claimed in the comments. Additionally, there is one pointless loop in the example code. In my errata, I use numerical codes and types to address all this.
Please test the code thoroughly.
=== Verifier note
Many of the changes in this erratum are not about errors, but more a refresh, better readability, simplification, etc. See https://mailarchive.ietf.org/arch/msg/v6ops/RnxLhcrAI4JmF8K7BRiW6xwTtkE/
