]> git.puffer.fish Git - mirror/frr.git/commitdiff
bgpd: Implement `neighbor X remote-as auto`
authorDonatas Abraitis <donatas@opensourcerouting.org>
Thu, 4 Jul 2024 11:42:19 +0000 (14:42 +0300)
committerDonatas Abraitis <donatas@opensourcerouting.org>
Thu, 4 Jul 2024 11:42:19 +0000 (14:42 +0300)
In some cases (large scale) it's desired to avoid changing configurations, but
let the BGP to automatically handle ASN changes.

`auto` means the peering can be iBGP or eBGP. It will be automatically detected
and adjusted from the OPEN message.

Signed-off-by: Donatas Abraitis <donatas@opensourcerouting.org>
12 files changed:
bgpd/bgp_attr.c
bgpd/bgp_packet.c
bgpd/bgp_updgrp.c
bgpd/bgp_vty.c
bgpd/bgpd.c
bgpd/bgpd.h
doc/user/bgp.rst
tests/topotests/bgp_remote_as_auto/__init__.py [new file with mode: 0644]
tests/topotests/bgp_remote_as_auto/r1/frr.conf [new file with mode: 0644]
tests/topotests/bgp_remote_as_auto/r2/frr.conf [new file with mode: 0644]
tests/topotests/bgp_remote_as_auto/r3/frr.conf [new file with mode: 0644]
tests/topotests/bgp_remote_as_auto/test_bgp_remote_as_auto.py [new file with mode: 0644]

index 18c7b13535d04e9c05670ff00d2f90dc5aa677a5..f2f7cfa93da5abc241443b868100da49b7b0ee44 100644 (file)
@@ -4468,6 +4468,8 @@ bgp_size_t bgp_packet_attribute(struct bgp *bgp, struct peer *peer,
                bgp_packet_mpattr_end(s, mpattrlen_pos);
        }
 
+       (void)peer_sort(peer);
+
        /* Origin attribute. */
        stream_putc(s, BGP_ATTR_FLAG_TRANS);
        stream_putc(s, BGP_ATTR_ORIGIN);
index 9a047b9d456ff19af6c6fdd70f1cfc4e9c66f418..4625f15778a2f80c2e29910f9a04df2e3271c63e 100644 (file)
@@ -1977,6 +1977,14 @@ static int bgp_open_receive(struct peer_connection *connection,
                                          BGP_NOTIFY_OPEN_BAD_PEER_AS,
                                          notify_data_remote_as, 2);
                return BGP_Stop;
+       } else if (peer->as_type == AS_AUTO) {
+               if (remote_as == peer->bgp->as) {
+                       peer->as = peer->local_as;
+                       SET_FLAG(peer->as_type, AS_INTERNAL);
+               } else {
+                       peer->as = remote_as;
+                       SET_FLAG(peer->as_type, AS_EXTERNAL);
+               }
        } else if (peer->as_type == AS_INTERNAL) {
                if (remote_as != peer->bgp->as) {
                        if (bgp_debug_neighbor_events(peer))
index 124e7a388b82cb950d827021d9b6e82fcd72ed82..b717793a4568a8c068d2b72d0df44884badaafba 100644 (file)
@@ -343,7 +343,12 @@ static unsigned int updgrp_hash_key_make(const void *p)
 
        key = 0;
 
-       key = jhash_1word(peer->sort, key); /* EBGP or IBGP */
+       /* `remote-as auto` technically uses identical peer->sort.
+        * After OPEN message is parsed, this is updated accordingly, but
+        * we need to call the peer_sort() here also to properly create
+        * separate subgroups.
+        */
+       key = jhash_1word(peer_sort((struct peer *)peer), key);
        key = jhash_1word(peer->sub_sort, key); /* OAD */
        key = jhash_1word((peer->flags & PEER_UPDGRP_FLAGS), key);
        key = jhash_1word((flags & PEER_UPDGRP_AF_FLAGS), key);
index e9a79766ec848c6a02f9d06b68b9c4b9a0611cad..13911b6879382ee2c2492257262e4308114c4b5d 100644 (file)
@@ -4871,6 +4871,9 @@ static int peer_remote_as_vty(struct vty *vty, const char *peer_str,
        } else if (as_str[0] == 'e') {
                as = 0;
                as_type = AS_EXTERNAL;
+       } else if (as_str[0] == 'a') {
+               as = 0;
+               as_type = AS_AUTO;
        } else if (!asn_str2asn(as_str, &as))
                as_type = AS_UNSPECIFIED;
 
@@ -4976,13 +4979,14 @@ ALIAS(no_bgp_shutdown, no_bgp_shutdown_msg_cmd,
 
 DEFUN (neighbor_remote_as,
        neighbor_remote_as_cmd,
-       "neighbor <A.B.C.D|X:X::X:X|WORD> remote-as <ASNUM|internal|external>",
+       "neighbor <A.B.C.D|X:X::X:X|WORD> remote-as <ASNUM|internal|external|auto>",
        NEIGHBOR_STR
        NEIGHBOR_ADDR_STR2
        "Specify a BGP neighbor\n"
        AS_STR
        "Internal BGP peer\n"
-       "External BGP peer\n")
+       "External BGP peer\n"
+       "Automatically detect remote ASN\n")
 {
        int idx_peer = 1;
        int idx_remote_as = 3;
@@ -5054,6 +5058,8 @@ static int peer_conf_interface_get(struct vty *vty, const char *conf_if,
                        as_type = AS_INTERNAL;
                } else if (as_str[0] == 'e') {
                        as_type = AS_EXTERNAL;
+               } else if (as_str[0] == 'a') {
+                       as_type = AS_AUTO;
                } else {
                        /* Get AS number.  */
                        if (asn_str2asn(as_str, &as))
@@ -5170,14 +5176,15 @@ DEFUN (neighbor_interface_config_v6only,
 
 DEFUN (neighbor_interface_config_remote_as,
        neighbor_interface_config_remote_as_cmd,
-       "neighbor WORD interface remote-as <ASNUM|internal|external>",
+       "neighbor WORD interface remote-as <ASNUM|internal|external|auto>",
        NEIGHBOR_STR
        "Interface name or neighbor tag\n"
        "Enable BGP on interface\n"
        "Specify a BGP neighbor\n"
        AS_STR
        "Internal BGP peer\n"
-       "External BGP peer\n")
+       "External BGP peer\n"
+       "Automatically detect remote ASN\n")
 {
        int idx_word = 1;
        int idx_remote_as = 4;
@@ -5187,7 +5194,7 @@ DEFUN (neighbor_interface_config_remote_as,
 
 DEFUN (neighbor_interface_v6only_config_remote_as,
        neighbor_interface_v6only_config_remote_as_cmd,
-       "neighbor WORD interface v6only remote-as <ASNUM|internal|external>",
+       "neighbor WORD interface v6only remote-as <ASNUM|internal|external|auto>",
        NEIGHBOR_STR
        "Interface name or neighbor tag\n"
        "Enable BGP with v6 link-local only\n"
@@ -5195,7 +5202,8 @@ DEFUN (neighbor_interface_v6only_config_remote_as,
        "Specify a BGP neighbor\n"
        AS_STR
        "Internal BGP peer\n"
-       "External BGP peer\n")
+       "External BGP peer\n"
+       "Automatically detect remote ASN\n")
 {
        int idx_word = 1;
        int idx_remote_as = 5;
@@ -5232,14 +5240,15 @@ DEFUN (neighbor_peer_group,
 
 DEFUN (no_neighbor,
        no_neighbor_cmd,
-       "no neighbor <WORD|<A.B.C.D|X:X::X:X> [remote-as <(1-4294967295)|internal|external>]>",
+       "no neighbor <WORD|<A.B.C.D|X:X::X:X> [remote-as <(1-4294967295)|internal|external|auto>]>",
        NO_STR
        NEIGHBOR_STR
        NEIGHBOR_ADDR_STR2
        "Specify a BGP neighbor\n"
        AS_STR
        "Internal BGP peer\n"
-       "External BGP peer\n")
+       "External BGP peer\n"
+       "Automatically detect remote ASN\n")
 {
        VTY_DECLVAR_CONTEXT(bgp, bgp);
        int idx_peer = 2;
@@ -5310,7 +5319,7 @@ DEFUN (no_neighbor,
 
 DEFUN (no_neighbor_interface_config,
        no_neighbor_interface_config_cmd,
-       "no neighbor WORD interface [v6only] [peer-group PGNAME] [remote-as <(1-4294967295)|internal|external>]",
+       "no neighbor WORD interface [v6only] [peer-group PGNAME] [remote-as <(1-4294967295)|internal|external|auto>]",
        NO_STR
        NEIGHBOR_STR
        "Interface name\n"
@@ -5321,7 +5330,8 @@ DEFUN (no_neighbor_interface_config,
        "Specify a BGP neighbor\n"
        AS_STR
        "Internal BGP peer\n"
-       "External BGP peer\n")
+       "External BGP peer\n"
+       "Automatically detect remote ASN\n")
 {
        VTY_DECLVAR_CONTEXT(bgp, bgp);
        int idx_word = 2;
@@ -5378,14 +5388,15 @@ DEFUN (no_neighbor_peer_group,
 
 DEFUN (no_neighbor_interface_peer_group_remote_as,
        no_neighbor_interface_peer_group_remote_as_cmd,
-       "no neighbor WORD remote-as <ASNUM|internal|external>",
+       "no neighbor WORD remote-as <ASNUM|internal|external|auto>",
        NO_STR
        NEIGHBOR_STR
        "Interface name or neighbor tag\n"
        "Specify a BGP neighbor\n"
        AS_STR
        "Internal BGP peer\n"
-       "External BGP peer\n")
+       "External BGP peer\n"
+       "Automatically detect remote ASN\n")
 {
        VTY_DECLVAR_CONTEXT(bgp, bgp);
        int idx_word = 2;
@@ -11887,7 +11898,7 @@ static bool bgp_show_summary_is_peer_filtered(struct peer *peer,
        /* filter remote-as (internal|external) */
        if (as_type != AS_UNSPECIFIED) {
                if (peer->as_type == AS_SPECIFIED) {
-                       if (as_type == AS_INTERNAL) {
+                       if (CHECK_FLAG(as_type, AS_INTERNAL)) {
                                if (peer->as != peer->local_as)
                                        return true;
                        } else if (peer->as == peer->local_as)
@@ -12879,6 +12890,8 @@ DEFPY(show_ip_bgp_summary, show_ip_bgp_summary_cmd,
                        as_type = AS_INTERNAL;
                else if (argv[idx + 1]->arg[0] == 'e')
                        as_type = AS_EXTERNAL;
+               else if (argv[idx + 1]->arg[0] == 'a')
+                       as_type = AS_AUTO;
                else if (!asn_str2asn(argv[idx + 1]->arg, &as)) {
                        vty_out(vty,
                                "%% Invalid neighbor remote-as value: %s\n",
@@ -14002,9 +14015,10 @@ static void bgp_show_peer(struct vty *vty, struct peer *p, bool use_json,
                        json_object_boolean_true_add(json_neigh,
                                                     "localAsReplaceAs");
        } else {
-               if ((p->as_type == AS_SPECIFIED) ||
-                   (p->as_type == AS_EXTERNAL) ||
-                   (p->as_type == AS_INTERNAL)) {
+               if (p->as_type == AS_SPECIFIED ||
+                   CHECK_FLAG(p->as_type, AS_AUTO) ||
+                   CHECK_FLAG(p->as_type, AS_EXTERNAL) ||
+                   CHECK_FLAG(p->as_type, AS_INTERNAL)) {
                        vty_out(vty, "remote AS ");
                        vty_out(vty, ASN_FORMAT(bgp->asnotation), &p->as);
                        vty_out(vty, ", ");
@@ -14023,7 +14037,7 @@ static void bgp_show_peer(struct vty *vty, struct peer *p, bool use_json,
                                : "");
        }
        /* peer type internal or confed-internal */
-       if ((p->as == p->local_as) || (p->as_type == AS_INTERNAL)) {
+       if ((p->as == p->local_as) || (CHECK_FLAG(p->as_type, AS_INTERNAL))) {
                if (use_json) {
                        if (CHECK_FLAG(bgp->config, BGP_CONFIG_CONFEDERATION))
                                json_object_boolean_true_add(
@@ -17011,7 +17025,7 @@ static int bgp_show_one_peer_group(struct vty *vty, struct peer_group *group,
                                &conf->as);
                        vty_out(vty, "\n");
                }
-       } else if (conf->as_type == AS_INTERNAL) {
+       } else if (CHECK_FLAG(conf->as_type, AS_INTERNAL)) {
                if (json)
                        asn_asn2json(json, "remoteAs", group->bgp->as,
                                     group->bgp->asnotation);
@@ -17023,7 +17037,8 @@ static int bgp_show_one_peer_group(struct vty *vty, struct peer_group *group,
                        vty_out(vty, "\nBGP peer-group %s\n", group->name);
        }
 
-       if ((group->bgp->as == conf->as) || (conf->as_type == AS_INTERNAL)) {
+       if ((group->bgp->as == conf->as) ||
+           CHECK_FLAG(conf->as_type, AS_INTERNAL)) {
                if (json)
                        json_object_string_add(json_peer_group, "type",
                                               "internal");
@@ -18525,6 +18540,9 @@ static void bgp_config_write_peer_global(struct vty *vty, struct bgp *bgp,
                } else if (peer->as_type == AS_EXTERNAL) {
                        vty_out(vty, " remote-as external");
                        if_ras_printed = true;
+               } else if (CHECK_FLAG(peer->as_type, AS_AUTO)) {
+                       vty_out(vty, " remote-as auto");
+                       if_ras_printed = true;
                }
 
                vty_out(vty, "\n");
@@ -18547,6 +18565,9 @@ static void bgp_config_write_peer_global(struct vty *vty, struct bgp *bgp,
                                vty_out(vty,
                                        " neighbor %s remote-as external\n",
                                        addr);
+                       } else if (CHECK_FLAG(peer->as_type, AS_AUTO)) {
+                               vty_out(vty, " neighbor %s remote-as auto\n",
+                                       addr);
                        }
                }
 
@@ -18576,6 +18597,9 @@ static void bgp_config_write_peer_global(struct vty *vty, struct bgp *bgp,
                                vty_out(vty,
                                        " neighbor %s remote-as external\n",
                                        addr);
+                       } else if (CHECK_FLAG(peer->as_type, AS_AUTO)) {
+                               vty_out(vty, " neighbor %s remote-as auto\n",
+                                       addr);
                        }
                }
        }
index 17d94bd575188743a311971bbbfe43986e7ec878..ca752da6b4157e1201cb576cf8bb83c0170fa918 100644 (file)
@@ -1074,10 +1074,10 @@ static inline enum bgp_peer_sort peer_calc_sort(struct peer *peer)
 
        /* Peer-group */
        if (CHECK_FLAG(peer->sflags, PEER_STATUS_GROUP)) {
-               if (peer->as_type == AS_INTERNAL)
+               if (CHECK_FLAG(peer->as_type, AS_INTERNAL))
                        return BGP_PEER_IBGP;
 
-               else if (peer->as_type == AS_EXTERNAL)
+               if (CHECK_FLAG(peer->as_type, AS_EXTERNAL))
                        return BGP_PEER_EBGP;
 
                else if (peer->as_type == AS_SPECIFIED && peer->as) {
@@ -1132,17 +1132,20 @@ static inline enum bgp_peer_sort peer_calc_sort(struct peer *peer)
                                                return BGP_PEER_IBGP;
                                        else
                                                return BGP_PEER_EBGP;
-                               } else if (peer->group->conf->as_type
-                                          == AS_INTERNAL)
+                               } else if (CHECK_FLAG(peer->group->conf->as_type,
+                                                     AS_INTERNAL))
                                        return BGP_PEER_IBGP;
                                else
                                        return BGP_PEER_EBGP;
                        }
                        /* no AS information anywhere, let caller know */
                        return BGP_PEER_UNSPECIFIED;
-               } else if (peer->as_type != AS_SPECIFIED)
-                       return (peer->as_type == AS_INTERNAL ? BGP_PEER_IBGP
-                                                            : BGP_PEER_EBGP);
+               } else if (peer->as_type != AS_SPECIFIED) {
+                       if (CHECK_FLAG(peer->as_type, AS_INTERNAL))
+                               return BGP_PEER_IBGP;
+                       else if (CHECK_FLAG(peer->as_type, AS_EXTERNAL))
+                               return BGP_PEER_EBGP;
+               }
 
                return (local_as == 0 ? BGP_PEER_INTERNAL
                                      : local_as == peer->as ? BGP_PEER_IBGP
@@ -2201,10 +2204,10 @@ int peer_remote_as(struct bgp *bgp, union sockunion *su, const char *conf_if,
                                }
                        } else {
                                /* internal/external used, compare as-types */
-                               if (((peer_sort_type == BGP_PEER_IBGP)
-                                   && (as_type != AS_INTERNAL))
-                                   || ((peer_sort_type == BGP_PEER_EBGP)
-                                   && (as_type != AS_EXTERNAL)))  {
+                               if (((peer_sort_type == BGP_PEER_IBGP) &&
+                                    !CHECK_FLAG(as_type, AS_INTERNAL)) ||
+                                   ((peer_sort_type == BGP_PEER_EBGP) &&
+                                    !CHECK_FLAG(as_type, AS_EXTERNAL))) {
                                        *as = peer->as;
                                        return BGP_ERR_PEER_GROUP_PEER_TYPE_DIFFERENT;
                                }
index 3c5826113dec72637acdecb39d3c8e619bba6235..709411b06eb7949d53ff19384c668a920b71af05 100644 (file)
@@ -56,10 +56,12 @@ struct bgp_pbr_config;
  * behavior
  * in the system.
  */
-enum { AS_UNSPECIFIED = 0,
-       AS_SPECIFIED,
-       AS_INTERNAL,
-       AS_EXTERNAL,
+enum peer_asn_type {
+       AS_UNSPECIFIED = 1,
+       AS_SPECIFIED = 2,
+       AS_INTERNAL = 4,
+       AS_EXTERNAL = 8,
+       AS_AUTO = 16,
 };
 
 /* Zebra Gracaful Restart states */
index 98834c7c23491d15af715fa1eafd4ca76ecdc7bb..a569a9af2889248926d7b78140cd2ef7a3ff2038 100644 (file)
@@ -1561,6 +1561,10 @@ Defining Peers
    peers ASN is the same as mine as specified under the :clicmd:`router bgp ASN`
    command the connection will be denied.
 
+.. clicmd:: neighbor PEER remote-as auto
+
+   The neighbor's ASN is detected automatically from the OPEN message.
+
 .. clicmd:: neighbor PEER oad
 
    Mark a peer belonging to the One Administrative Domain.
@@ -1699,7 +1703,7 @@ Configuring Peers
    IPv4 session addresses, see the ``neighbor PEER update-source`` command
    below.
 
-.. clicmd:: neighbor PEER interface remote-as <internal|external|ASN>
+.. clicmd:: neighbor PEER interface remote-as <internal|external|auto|ASN>
 
    Configure an unnumbered BGP peer. ``PEER`` should be an interface name. The
    session will be established via IPv6 link locals. Use ``internal`` for iBGP
diff --git a/tests/topotests/bgp_remote_as_auto/__init__.py b/tests/topotests/bgp_remote_as_auto/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/topotests/bgp_remote_as_auto/r1/frr.conf b/tests/topotests/bgp_remote_as_auto/r1/frr.conf
new file mode 100644 (file)
index 0000000..aec0e76
--- /dev/null
@@ -0,0 +1,17 @@
+!
+int r1-eth0
+ ip address 192.168.1.1/24
+!
+router bgp 65001
+ no bgp ebgp-requires-policy
+ no bgp network import-check
+ neighbor 192.168.1.2 remote-as auto
+ neighbor 192.168.1.2 timers 1 3
+ neighbor 192.168.1.2 timers connect 1
+ neighbor 192.168.1.3 remote-as auto
+ neighbor 192.168.1.3 timers 1 3
+ neighbor 192.168.1.3 timers connect 1
+ address-family ipv4 unicast
+  network 10.0.0.1/32
+ exit-address-family
+!
diff --git a/tests/topotests/bgp_remote_as_auto/r2/frr.conf b/tests/topotests/bgp_remote_as_auto/r2/frr.conf
new file mode 100644 (file)
index 0000000..f8d19a0
--- /dev/null
@@ -0,0 +1,10 @@
+!
+int r2-eth0
+ ip address 192.168.1.2/24
+!
+router bgp 65001
+ no bgp ebgp-requires-policy
+ neighbor 192.168.1.1 remote-as auto
+ neighbor 192.168.1.1 timers 1 3
+ neighbor 192.168.1.1 timers connect 1
+!
diff --git a/tests/topotests/bgp_remote_as_auto/r3/frr.conf b/tests/topotests/bgp_remote_as_auto/r3/frr.conf
new file mode 100644 (file)
index 0000000..fc68627
--- /dev/null
@@ -0,0 +1,10 @@
+!
+int r3-eth0
+ ip address 192.168.1.3/24
+!
+router bgp 65003
+ no bgp ebgp-requires-policy
+ neighbor 192.168.1.1 remote-as auto
+ neighbor 192.168.1.1 timers 1 3
+ neighbor 192.168.1.1 timers connect 1
+!
diff --git a/tests/topotests/bgp_remote_as_auto/test_bgp_remote_as_auto.py b/tests/topotests/bgp_remote_as_auto/test_bgp_remote_as_auto.py
new file mode 100644 (file)
index 0000000..b932920
--- /dev/null
@@ -0,0 +1,130 @@
+#!/usr/bin/env python
+# SPDX-License-Identifier: ISC
+
+# Copyright (c) 2024 by
+# Donatas Abraitis <donatas@opensourcerouting.org>
+#
+
+import os
+import re
+import sys
+import json
+import pytest
+import functools
+
+CWD = os.path.dirname(os.path.realpath(__file__))
+sys.path.append(os.path.join(CWD, "../"))
+
+# pylint: disable=C0413
+from lib import topotest
+from lib.topogen import Topogen, get_topogen
+
+pytestmark = [pytest.mark.bgpd]
+
+
+def setup_module(mod):
+    topodef = {"s1": ("r1", "r2", "r3")}
+    tgen = Topogen(topodef, mod.__name__)
+    tgen.start_topology()
+
+    router_list = tgen.routers()
+
+    for _, (rname, router) in enumerate(router_list.items(), 1):
+        router.load_frr_config(os.path.join(CWD, "{}/frr.conf".format(rname)))
+
+    tgen.start_router()
+
+
+def teardown_module(mod):
+    tgen = get_topogen()
+    tgen.stop_topology()
+
+
+def test_bgp_remote_as_auto():
+    tgen = get_topogen()
+
+    if tgen.routers_have_failure():
+        pytest.skip(tgen.errors)
+
+    r1 = tgen.gears["r1"]
+    r2 = tgen.gears["r2"]
+    r3 = tgen.gears["r3"]
+
+    def _bgp_converge():
+        output = json.loads(r1.vtysh_cmd("show bgp ipv4 unicast summary json"))
+        expected = {
+            "peers": {
+                "192.168.1.2": {
+                    "hostname": "r2",
+                    "remoteAs": 65001,
+                    "localAs": 65001,
+                    "state": "Established",
+                },
+                "192.168.1.3": {
+                    "hostname": "r3",
+                    "remoteAs": 65003,
+                    "localAs": 65001,
+                    "state": "Established",
+                },
+            }
+        }
+        return topotest.json_cmp(output, expected)
+
+    test_func = functools.partial(
+        _bgp_converge,
+    )
+    _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
+    assert result is None, "Can't see automatic iBGP/eBGP peerings"
+
+    def _bgp_converge_internal():
+        output = json.loads(r2.vtysh_cmd("show bgp ipv4 unicast 10.0.0.1/32 json"))
+        expected = {
+            "paths": [
+                {
+                    "aspath": {
+                        "string": "Local",
+                    },
+                    "valid": True,
+                    "peer": {
+                        "hostname": "r1",
+                        "type": "internal",
+                    },
+                }
+            ]
+        }
+        return topotest.json_cmp(output, expected)
+
+    test_func = functools.partial(
+        _bgp_converge_internal,
+    )
+    _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
+    assert result is None, "Can't see automatic iBGP peering"
+
+    def _bgp_converge_external():
+        output = json.loads(r3.vtysh_cmd("show bgp ipv4 unicast 10.0.0.1/32 json"))
+        expected = {
+            "paths": [
+                {
+                    "aspath": {
+                        "string": "65001",
+                    },
+                    "valid": True,
+                    "peer": {
+                        "hostname": "r1",
+                        "type": "external",
+                    },
+                }
+            ]
+        }
+        return topotest.json_cmp(output, expected)
+
+    test_func = functools.partial(
+        _bgp_converge_external,
+    )
+    _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
+    assert result is None, "Can't see automatic eBGP peering"
+
+
+if __name__ == "__main__":
+    args = ["-s"] + sys.argv[1:]
+    sys.exit(pytest.main(args))