{% from 'macros.j2' import write_prefix_list %}
{% from 'macros.j2' import write_prefix_list_entry %}
{% from 'macros.j2' import add_communities %}
{% from 'macros.j2' import match_communities %}

{% macro write_max_pref_action(action) %}
{% if action == "shutdown" %}disable;
{% elif action == "restart" %}restart;
{% elif action == "block" %}block;
{% else %}warn;
{% endif %}
{% endmacro %}

{% macro reject(client, cause, err_msg, avoid_braces=False) %}
{% if client.cfg.filtering.reject_policy.policy == "reject" %}
reject {{ err_msg }};
{% elif client.cfg.filtering.reject_policy.policy == "tag_and_reject" %}
{%	if not avoid_braces %}{{ "{ " }}{% endif %}tag_and_reject({{ cause}}, {{ client.asn }}); reject {{ err_msg }};{% if not avoid_braces %}{{ " }" }}{% endif %}
{% else %}
{%	if not avoid_braces %}{{ "{ " }}{% endif %}tag_and_reject({{ cause}}, {{ client.asn }}); print {{ err_msg }}; accept;{% if not avoid_braces %}{{ " }" }}{% endif %}
{% endif %}
{% endmacro %}
# ---------------------------------------------------------
# MEMBERS

{% for client in clients|sort(attribute="id") if client.ip is current_ipver %}

# AS-SET for {{ client.id }}
function origin_as_is_in_{{ client.id }}_as_set() {
{% if client.cfg.filtering.irrdb.as_set_bundle_ids %}
{%	for as_set_bundle_id in client.cfg.filtering.irrdb.as_set_bundle_ids|sort %}
{%		if irrdb_info[as_set_bundle_id].asns %}
	if bgp_path.last ~ AS_SET_{{ irrdb_info[as_set_bundle_id].name }}_asns then
		return true;
{%		else %}
	# AS-SET {{ irrdb_info[as_set_bundle_id].name }} referenced but empty.
{%		endif %}
{%	endfor %}
{% endif %}
	return false;
}

# R-SET for {{ client.id }}
function prefix_is_in_{{ client.id }}_as_set() {
{% if client.cfg.filtering.irrdb.as_set_bundle_ids %}
{%	for as_set_bundle_id in client.cfg.filtering.irrdb.as_set_bundle_ids|sort %}
{%    set this_ip_ver = client.ip|ipaddr_ver %}
{%    set prefixes = irrdb_info[as_set_bundle_id].prefixes|selectattr("prefix", "is_ipver", this_ip_ver)|list %}
{%    if prefixes %}
{%      if "2.0"|target_version_ge %}
    if net.type = NET_IP{{ this_ip_ver }} then
        if net ~ AS_SET_{{ irrdb_info[as_set_bundle_id].name }}_prefixes_{{ this_ip_ver }} then
            return true;
{%      else %}
    if net ~ AS_SET_{{ irrdb_info[as_set_bundle_id].name }}_prefixes_{{ this_ip_ver }} then
        return true;
{%      endif %}
{%    else %}
	# AS-SET {{ irrdb_info[as_set_bundle_id].name }} referenced but empty.
{%    endif %}
{%	endfor %}
{% endif %}
    return false;
}

function next_hop_is_valid_for_{{ client.id }}()
{
	# Checks if NEXT_HOP is one of those allowed for routes announced by {{ client.id }}
{% if client.cfg.filtering.next_hop.policy == "strict" %}
	if bgp_next_hop = {{ client.ip }} then return true;
{% elif client.cfg.filtering.next_hop.policy == "authorized_addresses" %}
{%	for addr in client.cfg.filtering.next_hop.authorized_addresses_list|sort if addr|ipaddr_ver == client.ip|ipaddr_ver %}
	if bgp_next_hop = {{ addr }} then return true;
{%	endfor %}
{% else %}
{%	for asn, same_as_clients in clients|groupby("asn") if asn == client.asn %}
{%		for same_as_client in same_as_clients|sort(attribute="ip") if same_as_client.ip|ipaddr_ver == client.ip|ipaddr_ver %}
	if bgp_next_hop = {{ same_as_client.ip }} then return true;	# {{ same_as_client.id }}
{%		endfor %}
{%	endfor %}
{% endif %}
	return false;
}

{% if client.cfg.filtering.black_list_pref %}
function prefix_is_in_{{ client.id }}_blacklist()
prefix set {{ client.id }}_blacklist;
{
	{{ client.id }}_blacklist = [
{{ write_prefix_list(client.cfg.filtering.black_list_pref|selectattr("prefix", "is_ipver", client.ip|ipaddr_ver)) }}
	];
	return net ~ {{ client.id }}_blacklist;
}
{% endif %}

{% if client.cfg.filtering.irrdb.enforce_origin_in_as_set or client.cfg.filtering.irrdb.enforce_prefix_in_as_set or cfg.filtering.irrdb.tag_as_set %}
function verify_{{ client.id }}_irrdb()
bool origin_ok;
bool prefix_ok;
bool validated;
{
	origin_ok = false;
	prefix_ok = false;
	validated = false;

{% 	if client.cfg.filtering.irrdb.enforce_origin_in_as_set or cfg.filtering.irrdb.tag_as_set %}
	origin_ok = origin_as_is_in_{{ client.id }}_as_set();
{%	endif %}
{%	if client.cfg.filtering.irrdb.enforce_prefix_in_as_set or cfg.filtering.irrdb.tag_as_set %}
	prefix_ok = prefix_is_in_{{ client.id }}_as_set();
{%	endif %}

{%	if cfg.filtering.irrdb.tag_as_set and cfg.communities.origin_not_present_in_as_set|community_is_set %}
	if !origin_ok then {
{{		add_communities(cfg.communities.origin_not_present_in_as_set) }}
	}
{%	endif %}
{%	if cfg.filtering.irrdb.tag_as_set and cfg.communities.origin_present_in_as_set|community_is_set %}
	if origin_ok then {
{{		add_communities(cfg.communities.origin_present_in_as_set) }}
	}
{%	endif %}
{%	if cfg.filtering.irrdb.tag_as_set and cfg.communities.prefix_not_present_in_as_set|community_is_set %}
	if !prefix_ok then {
{{		add_communities(cfg.communities.prefix_not_present_in_as_set) }}
	}
{%	endif %}
{%	if cfg.filtering.irrdb.tag_as_set and cfg.communities.prefix_present_in_as_set|community_is_set %}
	if prefix_ok then {
{{		add_communities(cfg.communities.prefix_present_in_as_set) }}
	}
{%	endif %}

{%	if cfg.filtering.irrdb.use_rpki_roas_as_route_objects.enabled %}
	# Origin ASN is valid, prefix is not: check if a RPKI ROAs exists.
	if origin_ok && prefix_in_rpki_roas_as_route_objects() then {
{%		if cfg.filtering.irrdb.tag_as_set and
			cfg.communities.prefix_validated_via_rpki_roas|community_is_set %}
{{			add_communities(cfg.communities.prefix_validated_via_rpki_roas) }}
{%		endif %}
		validated = true;
	}
{%	endif %}

{%	if cfg.filtering.irrdb.use_arin_bulk_whois_data.enabled and arin_whois_records %}
	# Origin ASN is valid, prefix is not: check if an ARIN Whois record exists.
	if origin_ok && prefix_in_arin_whois_db() then {
{%		if cfg.filtering.irrdb.tag_as_set and
			cfg.communities.prefix_validated_via_arin_whois_db_dump|community_is_set %}
{{			add_communities(cfg.communities.prefix_validated_via_arin_whois_db_dump) }}
{%		endif %}
		validated = true;
	}
{%	endif %}

{%	if cfg.filtering.irrdb.use_registrobr_bulk_whois_data.enabled and registrobr_whois_records %}
	# Origin ASN is valid, prefix is not: check if a NIC.BR Whois record exists.
	if origin_ok && prefix_in_registrobr_whois_db() then {
{%		if cfg.filtering.irrdb.tag_as_set and
			cfg.communities.prefix_validated_via_registrobr_whois_db_dump|community_is_set %}
{{			add_communities(cfg.communities.prefix_validated_via_registrobr_whois_db_dump) }}
{%		endif %}
		validated = true;
	}
{%	endif %}

{%	if client.cfg.filtering.irrdb.white_list_route %}
	# Client's white list
{%		for route in client.cfg.filtering.irrdb.white_list_route|selectattr("prefix", "is_ipver", client.ip|ipaddr_ver)|sort(attribute="prefix") if route.prefix|ipaddr_ver == client.ip|ipaddr_ver %}
	if !validated && net ~ [ {{ write_prefix_list_entry(route) }} ] then {
{%			if route.asn %}
		if bgp_path.last = {{ route.asn }} then {
{%				if cfg.filtering.irrdb.tag_as_set and cfg.communities.route_validated_via_white_list|community_is_set %}
{{					add_communities(cfg.communities.route_validated_via_white_list) }}
{%				endif %}
			validated = true;
		}
{%			else %}
{%				if cfg.filtering.irrdb.tag_as_set and cfg.communities.route_validated_via_white_list|community_is_set %}
{{					add_communities(cfg.communities.route_validated_via_white_list) }}
{%				endif %}
		validated = true;
{%			endif %}
	}
{%		endfor %}
{%	endif %}

{%	if client.cfg.filtering.irrdb.enforce_origin_in_as_set %}
	if !validated && !origin_ok then {
		{{ reject(client, 9, '"origin ASN [", bgp_path.last, "] not in allowed as-sets - REJECTING ", net', avoid_braces=True) }}
	}
{%	endif %}
{%	if client.cfg.filtering.irrdb.enforce_prefix_in_as_set %}
	if !validated && !prefix_ok then {
		{{ reject(client, 12, '"prefix not in client\'s r_set - REJECTING ", net', avoid_braces=True) }}
	}
{%	endif %}
}
{% endif %}

filter receive_from_{{ client.id }} {
	if !(source = RTS_BGP ) then
		{{ reject(client, 65535, '"source != RTS_BGP - REJECTING ", net') }}

	{% if "2.0"|target_version_ge %}
	if !(net.type = NET_IP{{ client.ip|ipaddr_ver }}) then
		{{ reject(client, 65535, '"AFI not enabled for this peer - REJECTING ", net') }}
	{% endif %}

	{% if "pre_receive_from_client"|hook_is_set %}
	if !hook_pre_receive_from_client({{ client.asn }}, {{ client.ip }}, "{{ client.id }}") then
		{{ reject(client, 65535, '"hook_pre_receive_from_client returned false - REJECTING ", net') }}
	{% endif %}

	scrub_communities_in();

{%	if client.cfg.attach_custom_communities %}
	# Attach custom BGP communities
{%		for name in client.cfg.attach_custom_communities|sort %}
	# {{ name }}
{{			add_communities(cfg.custom_communities[name], tabs=1) }}
{%		endfor %}
{%	endif %}

	# NEXT_HOP
	if !next_hop_is_valid_for_{{ client.id }}() then
		{{ reject(client, 5, '"NEXT_HOP [", bgp_next_hop, "] not allowed - REJECTING ", net') }}

	# AS_PATH: length
	if bgp_path.len > {{ cfg.filtering.max_as_path_len }} then
		{{ reject(client, 1, '"AS_PATH len [", bgp_path.len ,"] longer than ' ~ cfg.filtering.max_as_path_len ~ ' - REJECTING ", net') }}

	# AS_PATH: left-most ASN
	if bgp_path.first != {{ client.asn }} then
		{{ reject(client, 6, '"invalid left-most ASN [", bgp_path.first, "] - REJECTING ", net') }}

	{% if client.cfg.filtering.reject_invalid_as_in_as_path %}
	# AS_PATH: invalid ASNs
	if as_path_contains_invalid_asn() then
		{{ reject(client, 7, '"AS_PATH [", bgp_path ,"] contains invalid ASN - REJECTING ", net') }}
	{% endif %}

	{% set client_transit_free_asns = cfg.filtering.transit_free.asns|select("ne", client.asn) %}
	{% if cfg.filtering.transit_free.action and client_transit_free_asns %}
	# AS_PATH: transit-free ASNs
	if bgp_path ~ [{{ client_transit_free_asns|sort|join(", ")}}] then
	{%	if cfg.filtering.transit_free.action == "reject" %}
		{{ reject(client, 8, '"AS_PATH [", bgp_path ,"] contains transit-free ASN - REJECTING ", net') }}
	{%	else %}
		print "AS_PATH [", bgp_path ,"] contains transit-free ASN - WARNING ", net;
	{%	endif %}
	{% endif %}

	{% if never_via_route_servers_asns %}
	# AS_PATH: never via route-servers ASNs
	if bgp_path ~ [{{ never_via_route_servers_asns|sort|join(", ")}}] then
	{{ reject(client, 15, '"AS_PATH [", bgp_path ,"] contains never via route-servers ASN - REJECTING ", net') }}
	{% endif %}

	{% if client.ip|ipaddr_ver == 6 %}
	# Prefix: only IPv6 Global Unicast space allowed
	if !(net ~ [2000::/3+]) then
		{{ reject(client, 10, '"prefix is not in IPv6 Global Unicast space - REJECTING ", net') }}
	{% endif %}

	# Prefix: global blacklist
	if prefix_is_in_global_blacklist() then
		{{ reject(client, 3, '"prefix is in global blacklist - REJECTING ", net') }}

	{% if client.cfg.filtering.black_list_pref %}
	# Prefix: client's blacklist
	if prefix_is_in_{{ client.id }}_blacklist() then
		{{ reject(client, 11, '"prefix is in client\'s blacklist - REJECTING ", net') }}
	{% endif %}

	# Prefix: bogon
	if prefix_is_bogon() then
		{{ reject(client, 2, '"prefix is bogon - REJECTING ", net') }}

	{% if client.cfg.filtering.irrdb.enforce_origin_in_as_set or client.cfg.filtering.irrdb.enforce_prefix_in_as_set or cfg.filtering.irrdb.tag_as_set %}
	# IRRdb checks
	verify_{{ client.id }}_irrdb();
	{% endif %}

	# Blackhole request?
	if is_blackhole_filtering_request() then {
		accept "blackhole filtering request from {{ client.id }} - ACCEPTING ", net;
	} else {
		{% if cfg.filtering.rpki_bgp_origin_validation.enabled %}
		perform_rpki_validation();

		{%	if client.cfg.filtering.rpki_bgp_origin_validation.reject_invalid %}
		if route_is_rpki_invalid() then
			{{ reject(client, 14, '"RPKI, route is INVALID - REJECTING ", net') }}
		{%	endif %}
		{% endif %}

		# Prefix: length
		{% if client.ip|ipaddr_ver == 4 %}
		{% set min_pref_len = client.cfg.filtering.ipv4_pref_len.min %}
		{% set max_pref_len = client.cfg.filtering.ipv4_pref_len.max %}
		{% else %}
		{% set min_pref_len = client.cfg.filtering.ipv6_pref_len.min %}
		{% set max_pref_len = client.cfg.filtering.ipv6_pref_len.max %}
		{% endif %}
		if !prefix_len_is_valid({{ min_pref_len }}, {{ max_pref_len }}) then
			{{ reject(client, 13, '"prefix len [", net.len, "] not in ' ~ min_pref_len ~ '-' ~ max_pref_len ~ ' - REJECTING ", net') }}

		{% if cfg.graceful_shutdown.enabled %}
		{%	if client.cfg.graceful_shutdown.enabled %}
		honor_graceful_shutdown();
		{%	else %}
		prevent_graceful_shutdown();
		{%	endif %}
		{% endif %}
	}

	{% if "post_receive_from_client"|hook_is_set %}
	if !hook_post_receive_from_client({{ client.asn }}, {{ client.ip }}, "{{ client.id }}") then
		{{ reject(client, 65535, '"hook_post_receive_from_client returned false - REJECTING ", net') }}
	{% endif %}

	accept;
}

filter announce_to_{{ client.id }} {
	{% if "pre_announce_to_client"|hook_is_set %}
	if !hook_pre_announce_to_client({{ client.asn }}, {{ client.ip }}, "{{ client.id }}") then
		reject "hook_pre_announce_to_client returned false - NOT ANNOUNCING ", net, " TO {{ client.id}}";
	{% endif %}

	{% if cfg.communities.reject_cause|community_is_set %}
	{{- match_communities(cfg.communities.reject_cause, "reject;", replace_dyn_val="0") }}
	{% endif %}

	# Blackhole request?
	if is_blackhole_filtering_request() then {
	{% if not client.cfg.blackhole_filtering.announce_to_client %}
		reject "client {{ client.id }} not enabled to receive blackhole prefixes - NOT ANNOUNCING ", net, " TO {{ client.id}}";
	{% else %}
		apply_blackhole_filtering_policy();
	{% endif %}
	{% if cfg.filtering.rpki_bgp_origin_validation.enabled %}
	} else {
		# RPKI: do not announce INVALID to clients
		if route_is_rpki_invalid() then {
			if !announce_rpki_invalid_to_client({{ client.asn }}, {{ client.ip }}, "{{ client.id }}") then
				reject "route is INVALID - NOT ANNOUNCING ", net, " TO {{ client.id}}";
		}
	}
	{% else %}
	}
	{% endif %}

	# BGP control communities
	if !route_can_be_announced_to({{ client.asn }}, {{ client.ip }}, "{{ client.id }}") then
		reject "route didn't pass control communities checks - NOT ANNOUNCING ", net, " TO {{ client.id }}";

	apply_prepend({{ client.asn }}, {{ client.ip }});

	{% if client.cfg.prepend_rs_as %}
	# {{ client.id }} has been configured with 'prepend_rs_as'
	bgp_path.prepend({{ cfg.rs_as }});
	{% endif %}

	add_noexport_noadvertise({{ client.asn }});

	scrub_communities_out();

	{% if perform_graceful_shutdown %}
	perform_graceful_shutdown();
	{% endif %}

	{% if "post_announce_to_client"|hook_is_set %}
	if !hook_post_announce_to_client({{ client.asn }}, {{ client.ip }}, "{{ client.id }}") then
		reject "hook_post_announce_to_client returned false - NOT ANNOUNCING ", net, " TO {{ client.id}}";
	{% endif %}

	accept;
}

{% if "rtt" in client and client["rtt"] %}
# RTT: {{ client["rtt"] }} ms (normalized value: {{ client["rtt"]|get_normalized_rtt }})
{% endif %}
protocol bgp {{ client.id }} {
	{% if client.description %}
	description "{{ client.description|replace("\"", "") }}";
	{% endif %}

	local as {{ cfg.rs_as }};
	neighbor {{ client.ip }} as {{ client.asn }};
	rs client;

	{% if client.password %}
	password "{{ client.password }}";
	{% endif -%}

	{% if client.cfg.passive %}
	passive on;
	{% else %}
	passive off;
	{% endif -%}

	{% if client.cfg.gtsm %}
	ttl security on;
	{% else %}
	ttl security off;
	{% endif -%}

	{% if cfg.rfc1997_wellknown_communities.policy == "rfc1997" %}
	interpret communities on;
	{% else %}
	interpret communities off;
	{% endif %}

	{% if "2.0.0"|target_version_ge %}
	# ---------------------------------------
	ipv{{ client.ip|ipaddr_ver }} {
	table master{{ client.ip|ipaddr_ver }};
	{% endif %}

	{% if client.cfg.add_path %}
	add paths tx;
	{% endif %}

	{% if cfg.path_hiding %}
	secondary;
	{% endif %}

	{% if client.cfg.filtering.max_prefix.action %}
	{%	if client.ip|ipaddr_ver == 4 and client.cfg.filtering.max_prefix.limit_ipv4 %}
	{%		set max_pref_limit = client.cfg.filtering.max_prefix.limit_ipv4 %}
	{% 	elif client.ip|ipaddr_ver == 6 and client.cfg.filtering.max_prefix.limit_ipv6 %}
	{%      	set max_pref_limit = client.cfg.filtering.max_prefix.limit_ipv6 %}
	{% 	else %}
	{%		set max_pref_limit = 0 %}
	{% 	endif %}
	{% 	if max_pref_limit > 0 %}
	import limit {{ max_pref_limit }} action {{ write_max_pref_action(client.cfg.filtering.max_prefix.action) }}
	{% 	endif %}
	{% endif %}

	{% if "2.0.0"|target_version_ge %}
	import table on;
	{% endif %}

	import keep filtered on;

	import filter receive_from_{{ client.id }};
	export filter announce_to_{{ client.id }};

	{% if "2.0.0"|target_version_ge %}
	# ---------------------------------------
	};
	{% endif %}

	{{ "client"|include_local_file -}}
	{% if client.ip|ipaddr_ver == 4 %}
	{{ "client4"|include_local_file -}}
	{% else %}
	{{ "client6"|include_local_file -}}
	{% endif %}
}

{% endfor -%}
