nanog mailing list archives

Re: [BGP Hijack] AS202734 hijacked multiple Chinese Carriers on May 16-17 – Full evidence and attribution


From: Jacob-Junqi Tian via NANOG <nanog () lists nanog org>
Date: Sat, 23 May 2026 04:34:55 -0000

Hello,

Thanks for sharing this incident with the community. On May 17, because of an incorrect filter configuration, routers 
of AS202734 exported a number of prefixes from its internal routing table to the route collector of the Hurricane 
Electric BGP looking glass (bgp.he.net). 

For BGP sessions with upstream transit providers (real routers), AS202734 uses a tighter set of filters, and export 
only prefixes that are within a small number of whitelisted "ANNOUNCED_v4/v6" subnets. Thus, none of the said prefixes 
were ever exported to real routers on the internet, and this incident did not affect any public routing infrastructure. 
However, this incident might have triggered a number of automated emails from the HE.net looking glass to those who 
subscribed to updates about the affected prefixes. These are truly what one would describe as "false alarms". If you 
did receive one of these emails and was alerted, you have my apologies.

For the benefit of the NANOG community, here is a detailed description of what triggered this "false alarm" incident.

AS202734/AS402333 runs a custom fork of BIRD3 as the routing control plane for its routers. The configuration files for 
these routers are rendered automatically from a Jinja template- and the aforementioned export filter is no exception. 
The same filter template controls both routes exported to transit providers and public looking glass route collectors 
such as bgp.he.net.

When AS202734 receives a prefix from its upstreams or IX peers, the import filter would automatically tag the route 
with a large community of the form ({{ cold_potato_asn }}, {{ router_ld2_id }}, {{ neighbor_asn }})). This community 
identifies the source of the route, as well as the AS202734 router where the prefix was learned. 

BGP looking glasses- bgp.he.net, bgptools, etc.- typically require a full table to be exported- not just routes 
whitelisted for announcement to upstreams. Since the whitelist filter no longer applies, some alternative method would 
be necessary to filter out routes that are in the iBGP table, but are not learned from peers or upstreams- these 
include internal routing infrastructure prefixes, as well as other types of routing optimizations that should not be 
exported to the public internet. It seems like a natural option to build this filter around the aforementioned BGP 
Large Community, which is supposedly applied only to routes learned from other networks, and not from the internal 
infrastructure. As seen in line 30 of the Jinja template below, "session_export_non_local" evaluates to True for route 
collectors (but not for upstream transit providers,) and routes would be accepted for export to the collectors even if 
they are not in the "announced" whitelist- as long as they are tagged
  with this large community when learned from peers or upstream. In the filter for regular upstream transit providers, 
the original whitelist still applies. 

This setup worked just as expected for months, and routes observed at AS202734 were visible through various public 
looking glasses, helping other networks understand the reachability of their prefixes. However, up to this point, there 
is no clear way to determine from which session or geographic region was a particular prefix exported- either to the 
route collector or to a real upstream. To resolve this, it seems natural to reuse the same large community- tagging 
exported routes with geographical router info and session info in the export filter. See line 15 of the code snippet 
below: 

        # v6 version of the filter is shown; the filter for v4 follows the same logic
     1  filter ebgp_export_v6_{{ session_id }} {
     2  {%       for community in session_communities %}
     3  {%         set community_value = community if community is string else (community | join(', ')) %}
     4    bgp_community.add(({{ community_value }}));
     5  {%       endfor %}
     6  {%       for community in session_large_communities %}
     7  {%         set community_value = community if community is string else (community | join(', ')) %}
     8    bgp_large_community.add(({{ community_value }}));
     9  {%       endfor %}
    10  {%       for community in session_ext_communities %}
    11  {%         set community_value = community if community is string else (community | join(', ')) %}
    12    bgp_ext_community.add(({{ community_value }}));
    13  {%       endfor %}
    14
    15    # Added on the day of the incident:
            # bgp_large_community.add(({{ cold_potato_asn }}, {{ router_ld2_id }}, {{ neighbor_asn }}));
    16
    17  {%       for prepend in session_prepend %}
    18    bgp_path.prepend({{ prepend }});
    19  {%       endfor %}
    20
    21    if net = ::/0 then {
    22  {%       if session_export_default %}
    23      accept;
    24  {%       else %}
    25      reject;
    26  {%       endif %}
    27    }
    28
    29    if (net ~ ANNOUNCED_v6) then accept;
    30  {%       if session_export_non_local %}
    31    if (bgp_large_community ~ [ ({{ cold_potato_asn }}, *, *) ]) then accept;
    32  {%       endif %}
    33    reject;

As you might have anticipated, the issue is that this newly-added BGP large community applies to all prefixes routes in 
the table, and not just those directly learned from upstream/peers. As a result, a large number of internal-only 
prefixes (4,622 to be exact) with missing BGP PATH info also received this large community. While the whitelisting 
mechanism prevented these prefixes from being announced to upstream transit providers, these prefixes were announced to 
route collectors of BGP.he.net from two of the AS202734 routers.

The fix, as you might expect, is to move the filter "accept" logic into its own function, and apply any information 
communities only after if the "accept" function returned true and no "reject" filter was triggered. After deploying 
this fix and testing extensively, AS202734 is now once again contributing data to the public looking glasses- with the 
BGP large community included to help engineers of other networks better understand the reachability of their prefixes 
across geographies. To avoid similar incidents in the future, a staging environment paired with extensive code reviews 
might be helpful. 

I hereby thank the NANOG community and in particular Zhong Miao, the Independent Security Researcher, for bringing this 
issue to my attention so swiftly. I also appreciate comments from the community regarding best practices to make the 
deployment process more reliable and secure.

Regards,
Jacob-Junqi Tian
May. 23, 2026.
_______________________________________________
NANOG mailing list 
https://lists.nanog.org/archives/list/nanog () lists nanog org/message/DOAA6GMU234E5XXMNYR53F7MMGBHHILD/


Current thread: