├── routers ├── bird │ ├── tests │ │ ├── __init__.py │ │ ├── data │ │ │ └── __init__.py │ │ └── test_bird.py │ ├── supervisord.conf │ ├── docker-compose.yml │ ├── pytest.ini │ ├── Dockerfile │ ├── entrypoint.sh │ ├── __init__.py │ ├── templates │ │ └── bird.conf.j2 │ └── README.md ├── frr │ ├── tests │ │ ├── __init__.py │ │ ├── test_frr.py │ │ └── data │ │ │ └── __init__.py │ ├── entrypoint.sh │ ├── docker-compose.yml │ ├── pytest.ini │ ├── Dockerfile │ ├── templates │ │ └── frr.conf.j2 │ ├── __init__.py │ ├── daemons │ └── README.md ├── pytest.ini ├── helpers │ └── __init__.py ├── __init__.py └── test_data │ └── __init__.py ├── .github └── CODEOWNERS ├── .gitignore ├── README.md ├── Dockerfile ├── setup.py ├── .drone.yml ├── Makefile ├── configure.py └── LICENSE /routers/bird/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /routers/frr/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @packethost/governor-software-networking 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .mypy_cache 3 | .coverage 4 | *egg-info* 5 | -------------------------------------------------------------------------------- /routers/bird/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon = True 3 | user = root 4 | stdout_logfile = /proc/1/fd/1 5 | sterr_logfile = /proc/1/fd/2 6 | stdout_logfile_maxbytes = 0 7 | stderr_logfile_maxbytes = 0 8 | -------------------------------------------------------------------------------- /routers/frr/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ux 4 | 5 | /opt/bgp/configure.py -r frr | tee /etc/frr/frr.conf 6 | 7 | [ ${PIPESTATUS[0]} != 0 ] && exit ${PIPESTATUS[0]} 8 | 9 | /etc/init.d/frr start 10 | exec sleep 10000d 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Network Helpers 2 | 3 | This repo contains a collection of helper scripts for installing and configuring various network services on the Packet platform. 4 | 5 | ### BGP helpers 6 | 7 | * [Bird](routers/bird/README.md) 8 | * [FRR](routers/frr/README.md) 9 | -------------------------------------------------------------------------------- /routers/bird/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | 4 | bird: 5 | image: local/bird:latest 6 | container_name: bird 7 | network_mode: host 8 | privileged: true 9 | restart: unless-stopped 10 | ports: 11 | - "179:179" 12 | hostname: bird 13 | -------------------------------------------------------------------------------- /routers/frr/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | 4 | bird: 5 | image: local/frr:latest 6 | container_name: frr 7 | network_mode: host 8 | privileged: true 9 | restart: unless-stopped 10 | ports: 11 | - "179:179" 12 | hostname: frr 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-stretch 2 | 3 | RUN apt-get update && apt-get install -y make python-dev && \ 4 | apt-get clean && \ 5 | pip install --upgrade pip && \ 6 | pip install black mypy pytest pylama pytest-cov jmespath 7 | 8 | RUN mkdir /opt/tests 9 | 10 | ADD . /opt/tests 11 | WORKDIR /opt/tests 12 | 13 | RUN pip install -e ./ 14 | 15 | CMD ["make", "all"] 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | setup( 4 | name="routers", 5 | description="Various packages for auto-configuring Linux routing daemons", 6 | author="Packethost", 7 | packages=find_packages(), 8 | install_requires=[ 9 | "Jinja2 >= 2.11.1", 10 | "MarkupSafe >= 1.1.1", 11 | "requests >= 2.23.0", 12 | "urllib3 >= 1.25.8", 13 | "certifi >= 2019.11.28", 14 | "chardet >= 3.0.4", 15 | "idna >= 2.9", 16 | ], 17 | ) 18 | -------------------------------------------------------------------------------- /routers/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | ignore:.*ABC.* is deprecated:DeprecationWarning 4 | 5 | [pylama:pycodestyle] 6 | max_line_length = 100 7 | 8 | [pylama:pylint] 9 | max_line_length = 100 10 | 11 | [mypy] 12 | python_version = 3.7 13 | check_untyped_defs = True 14 | disallow_any_generics = True 15 | disallow_untyped_calls = True 16 | disallow_untyped_defs = True 17 | disallow_incomplete_defs = True 18 | ignore_errors = False 19 | ignore_missing_imports = True 20 | strict_optional = True 21 | warn_unused_ignores = True 22 | warn_redundant_casts = True 23 | warn_return_any = True 24 | warn_unused_configs = True 25 | -------------------------------------------------------------------------------- /routers/bird/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | ignore:.*ABC.* is deprecated:DeprecationWarning 4 | 5 | [pylama:pycodestyle] 6 | max_line_length = 100 7 | 8 | [pylama:pylint] 9 | max_line_length = 100 10 | 11 | [mypy] 12 | python_version = 3.7 13 | check_untyped_defs = True 14 | disallow_any_generics = True 15 | disallow_untyped_calls = True 16 | disallow_untyped_defs = True 17 | disallow_incomplete_defs = True 18 | ignore_errors = False 19 | ignore_missing_imports = True 20 | strict_optional = True 21 | warn_unused_ignores = True 22 | warn_redundant_casts = True 23 | warn_return_any = True 24 | warn_unused_configs = True 25 | -------------------------------------------------------------------------------- /routers/frr/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | filterwarnings = 3 | ignore:.*ABC.* is deprecated:DeprecationWarning 4 | 5 | [pylama:pycodestyle] 6 | max_line_length = 100 7 | 8 | [pylama:pylint] 9 | max_line_length = 100 10 | 11 | [mypy] 12 | python_version = 3.7 13 | check_untyped_defs = True 14 | disallow_any_generics = True 15 | disallow_untyped_calls = True 16 | disallow_untyped_defs = True 17 | disallow_incomplete_defs = True 18 | ignore_errors = False 19 | ignore_missing_imports = True 20 | strict_optional = True 21 | warn_unused_ignores = True 22 | warn_redundant_casts = True 23 | warn_return_any = True 24 | warn_unused_configs = True 25 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | pipeline: 2 | publish: 3 | group: publish 4 | image: plugins/docker 5 | registry: quay.io 6 | repo: quay.io/packet/network-helpers 7 | force_tag: true 8 | tags: 9 | - ${DRONE_BUILD_NUMBER}-${DRONE_COMMIT_SHA} 10 | secrets: [docker_username, docker_password] 11 | 12 | lint: 13 | group: test 14 | image: quay.io/packet/network-helpers:${DRONE_BUILD_NUMBER}-${DRONE_COMMIT_SHA} 15 | commands: 16 | - make lint 17 | 18 | test_bird: 19 | group: test 20 | image: quay.io/packet/network-helpers:${DRONE_BUILD_NUMBER}-${DRONE_COMMIT_SHA} 21 | commands: 22 | - make test-bird 23 | 24 | test_frr: 25 | group: test 26 | image: quay.io/packet/network-helpers:${DRONE_BUILD_NUMBER}-${DRONE_COMMIT_SHA} 27 | commands: 28 | - make test-frr 29 | -------------------------------------------------------------------------------- /routers/bird/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster 2 | 3 | ARG RELEASE=1.6.8 4 | 5 | RUN apt -y update && \ 6 | apt -y install git wget curl vim gcc binutils m4 make flex bison libncurses5-dev libreadline-dev python3.7-dev python3-pip pkg-config libcairo2-dev procps supervisor 7 | 8 | RUN cd /root && \ 9 | wget ftp://bird.network.cz/pub/bird/bird-${RELEASE}.tar.gz && \ 10 | tar -xzvf bird-${RELEASE}.tar.gz && \ 11 | cd bird-${RELEASE} && \ 12 | ./configure && \ 13 | make && \ 14 | make install 15 | 16 | RUN cd /root/bird-${RELEASE} && \ 17 | ./configure --enable-ipv6 && \ 18 | make && \ 19 | make install 20 | 21 | ADD . /opt/bgp 22 | 23 | WORKDIR /opt/bgp 24 | 25 | RUN pip3 install --upgrade pip setuptools jmespath && \ 26 | pip3 install -e /opt/bgp/ 27 | 28 | RUN mkdir /etc/bird 29 | 30 | EXPOSE 179 31 | 32 | ENTRYPOINT ["/opt/bgp/routers/bird/entrypoint.sh"] 33 | -------------------------------------------------------------------------------- /routers/frr/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y libpcre3-dev apt-transport-https ca-certificates curl wget logrotate libc-ares2 libjson-c3 vim systemd procps libreadline7 gnupg2 lsb-release apt-utils python3.7-dev python3-pip pkg-config libcairo2-dev procps supervisor 5 | 6 | RUN curl -s https://deb.frrouting.org/frr/keys.asc | apt-key add - && \ 7 | echo deb https://deb.frrouting.org/frr $(lsb_release -s -c) frr-stable | tee -a /etc/apt/sources.list.d/frr.list 8 | 9 | RUN apt-get update && \ 10 | apt-get install -y frr frr-pythontools 11 | 12 | ADD . /opt/bgp 13 | ADD ./routers/frr/daemons /etc/frr/daemons 14 | 15 | RUN chown -Rv frr:frr /etc/frr 16 | 17 | WORKDIR /opt/bgp 18 | 19 | RUN pip3 install --upgrade pip setuptools jmespath && \ 20 | pip3 install -e /opt/bgp/ 21 | 22 | EXPOSE 179 23 | 24 | ENTRYPOINT ["/opt/bgp/routers/frr/entrypoint.sh"] 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TEST_ROOT=/opt/tests 2 | ROUTERS_PATH=$(TEST_ROOT)/routers 3 | BIRD_PATH=$(TEST_ROOT)/routers/bird 4 | FRR_PATH=$(TEST_ROOT)/routers/frr 5 | 6 | .PHONY: clean-pyc 7 | clean-pyc: 8 | -find . -name '*.pyc' -delete 9 | -find . -name '*.pyo' -delete 10 | -find . -name '*~' -delete 11 | -find . -name '.mypy_cache' | xargs rm -rf 12 | -find . -name '.pytest_cache' | xargs rm -rf 13 | -find . -name '__pycache__' | xargs rm -rf 14 | 15 | .PHONY: lint 16 | lint: clean-pyc 17 | black --check $(TEST_ROOT) 18 | 19 | .PHONY: test-routers 20 | test-routers: 21 | mypy $(ROUTERS_PATH) --config-file $(ROUTERS_PATH)/pytest.ini 22 | 23 | .PHONY: test-bird 24 | test-bird: 25 | mypy $(BIRD_PATH) --config-file $(BIRD_PATH)/pytest.ini 26 | cd $(BIRD_PATH); python -m pytest --cov --pylama --verbose --color=yes 27 | 28 | .PHONY: test-frr 29 | test-frr: 30 | mypy $(FRR_PATH) --config-file $(FRR_PATH)/pytest.ini 31 | cd $(FRR_PATH); python -m pytest --cov --pylama --verbose --color=yes 32 | 33 | all: lint test-routers test-bird test-frr 34 | -------------------------------------------------------------------------------- /routers/bird/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eux 4 | 5 | # We should only start bird and bird6 if bgp over ipv4 and 6 | # ipv6 is respectively enabled 7 | 8 | /opt/bgp/configure.py -r bird | tee /etc/bird/bird.conf 9 | if [ ${PIPESTATUS[0]} == 0 ]; then 10 | echo >> /opt/bgp/routers/bird/supervisord.conf 11 | cat << EOF >> /opt/bgp/routers/bird/supervisord.conf 12 | [program:bird] 13 | command = bird -c /etc/bird/bird.conf -d 14 | user = root 15 | stdout_logfile = /proc/1/fd/1 16 | sterr_logfile = /proc/1/fd/2 17 | stdout_logfile_maxbytes = 0 18 | stderr_logfile_maxbytes = 0 19 | EOF 20 | else 21 | rm -vf /etc/bird/bird.conf 22 | fi 23 | 24 | /opt/bgp/configure.py -r bird6 | tee /etc/bird/bird6.conf 25 | if [ ${PIPESTATUS[0]} == 0 ]; then 26 | echo >> /opt/bgp/routers/bird/supervisord.conf 27 | cat << EOF >> /opt/bgp/routers/bird/supervisord.conf 28 | [program:bird6] 29 | command = bird6 -c /etc/bird/bird6.conf -d 30 | user = root 31 | stdout_logfile = /proc/1/fd/1 32 | sterr_logfile = /proc/1/fd/2 33 | stdout_logfile_maxbytes = 0 34 | stderr_logfile_maxbytes = 0 35 | EOF 36 | echo >> /opt/bgp/routers/bird/supervisord.conf 37 | else 38 | rm -vf /etc/bird/bird6.conf 39 | fi 40 | 41 | supervisord -c /opt/bgp/routers/bird/supervisord.conf 42 | -------------------------------------------------------------------------------- /configure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import os 4 | import sys 5 | 6 | from routers.bird import Bird 7 | from routers.frr import FRR 8 | from routers.helpers import fetch_bgp 9 | 10 | USE_METADATA = os.getenv("USE_METADATA", "yes") 11 | API_TOKEN = os.getenv("API_TOKEN", None) 12 | INSTANCE_ID = os.getenv("INSTANCE_ID", None) 13 | 14 | if __name__ == "__main__": # noqa: C901 15 | if USE_METADATA != "yes": 16 | if not API_TOKEN: 17 | sys.exit("Packet API token missing from environment") 18 | if not INSTANCE_ID: 19 | sys.exit("Instance ID missing from environment") 20 | 21 | parser = argparse.ArgumentParser() 22 | parser.add_argument( 23 | "-r", 24 | "--router", 25 | help="the routing daemon to be configured", 26 | action="store", 27 | type=str, 28 | choices=["bird", "bird6", "frr"], 29 | required=True, 30 | ) 31 | args = parser.parse_args() 32 | 33 | headers = {} 34 | if API_TOKEN: 35 | headers["X-Auth-Token"] = API_TOKEN 36 | 37 | bgp = fetch_bgp( 38 | use_metadata=(USE_METADATA == "yes"), headers=headers, instance=INSTANCE_ID 39 | ) 40 | 41 | if args.router == "bird": 42 | bird = Bird(**bgp) 43 | if bird.v4_peer_count > 0: 44 | print(bird.config) 45 | else: 46 | sys.exit("BGP over IPv4 is not enabled") 47 | elif args.router == "bird6": 48 | bird6 = Bird(family=6, **bgp) 49 | if bird6.v6_peer_count > 0: 50 | print(bird6.config) 51 | else: 52 | sys.exit("BGP over IPv6 is not enabled") 53 | elif args.router == "frr": 54 | frr = FRR(**bgp) 55 | print(frr.config) 56 | else: 57 | sys.exit("Unrecognized routing daemon specified") 58 | -------------------------------------------------------------------------------- /routers/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from json.decoder import JSONDecodeError 2 | from typing import Any, Dict, List, Optional 3 | 4 | import requests 5 | 6 | 7 | def fetch_ip_addresses( 8 | headers: Dict[str, str] = {}, instance: Optional[str] = None 9 | ) -> Any: 10 | url = "https://api.packet.net/devices/{}".format(instance) 11 | response = requests.get(url, headers=headers) 12 | try: 13 | response_payload = response.json() 14 | if "ip_addresses" not in response_payload: 15 | return [] 16 | else: 17 | return response_payload["ip_addresses"] 18 | except JSONDecodeError as e: 19 | raise JSONDecodeError( 20 | "Unable to decode API/metadata response for {}. {}".format(url, e.msg), 21 | e.doc, 22 | e.pos, 23 | ) 24 | 25 | 26 | def fetch_bgp( 27 | use_metadata: bool = True, 28 | headers: Dict[str, str] = {}, 29 | instance: Optional[str] = None, 30 | ) -> Any: 31 | url = "https://metadata.packet.net/metadata" 32 | ip_addresses = [] 33 | if not use_metadata: 34 | if not instance: 35 | raise ValueError("Instance ID must be specified when not using metadata") 36 | url = "https://api.packet.net/devices/{}/bgp/neighbors".format(instance) 37 | ip_addresses = fetch_ip_addresses(headers=headers, instance=instance) 38 | 39 | response = requests.get(url, headers=headers) 40 | 41 | try: 42 | response_payload = response.json() 43 | if not use_metadata: 44 | response_payload["network"] = {"addresses": ip_addresses} 45 | return response_payload 46 | except JSONDecodeError as e: 47 | raise JSONDecodeError( 48 | "Unable to decode API/metadata response for {}. {}".format(url, e.msg), 49 | e.doc, 50 | e.pos, 51 | ) 52 | -------------------------------------------------------------------------------- /routers/frr/templates/frr.conf.j2: -------------------------------------------------------------------------------- 1 | frr defaults traditional 2 | log syslog informational 3 | ipv6 forwarding 4 | service integrated-vtysh-config 5 | ! 6 | {%- for neighbor in data.bgp_neighbors.values() %} 7 | {%- for group in neighbor %} 8 | {%- if group.multihop %} 9 | {%- if group.address_family == 4 %} 10 | {%- for address in group.peer_ips %} 11 | ip route {{ address }}/32 {{ data.meta.ipv4_next_hop }} 12 | {%- endfor %} 13 | {%- else %} 14 | {%- for address in group.peer_ips %} 15 | ipv6 route {{ address }}/128 {{ data.meta.ipv6_next_hop }} 16 | {%- endfor %} 17 | {%- endif %} 18 | {%- endif %} 19 | {%- endfor %} 20 | {%- endfor %} 21 | ! 22 | {%- for asn, neighbor in data.bgp_neighbors.items() %} 23 | router bgp {{ asn }} 24 | bgp ebgp-requires-policy 25 | {%- for group in neighbor %} 26 | neighbor V{{ group.address_family }} peer-group 27 | neighbor V{{ group.address_family }} remote-as {{ group.peer_as }} 28 | {%- if group.md5_enabled %} 29 | neighbor V{{ group.address_family }} password {{ group.md5_password }} 30 | {%- endif %} 31 | {%- if group.multihop %} 32 | neighbor V{{ group.address_family }} ebgp-multihop 5 33 | {%- endif %} 34 | {%- for address in group.peer_ips %} 35 | neighbor {{ address }} peer-group V{{ group.address_family }} 36 | {%- endfor -%} 37 | {% endfor %} 38 | ! 39 | {%- for group in neighbor %} 40 | address-family ipv{{ group.address_family }} unicast 41 | redistribute connected 42 | {%- if group.address_family == 6 %} 43 | neighbor V{{ group.address_family }} activate 44 | {%- endif %} 45 | neighbor V{{ group.address_family }} route-map IMPORT in 46 | neighbor V{{ group.address_family }} route-map EXPORT out 47 | exit-address-family 48 | ! 49 | {%- endfor %} 50 | {%- endfor %} 51 | route-map EXPORT deny 100 52 | ! 53 | route-map EXPORT permit 1 54 | match interface lo 55 | ! 56 | route-map IMPORT deny 1 57 | ! 58 | line vty 59 | ! 60 | -------------------------------------------------------------------------------- /routers/bird/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Dict 3 | 4 | from jinja2 import Environment, FileSystemLoader 5 | from jinja2.exceptions import TemplateNotFound 6 | from routers import Router 7 | 8 | 9 | class Bird(Router): 10 | def __init__(self, family: int = 4, **kwargs: Any) -> None: 11 | super().__init__(**kwargs) 12 | self.family = family 13 | self.config = self.render_config(self.build_config(), "bird.conf.j2").strip() 14 | 15 | def build_config(self) -> Dict[str, Any]: 16 | if not self.router_id: 17 | raise LookupError("Unable to determine router id") 18 | 19 | ipv4_next_hop, ipv6_next_hop = self.multi_hop_gateway 20 | 21 | if self.ipv4_multi_hop and not ipv4_next_hop: 22 | raise LookupError("Unable to determine IPv4 next hop for multihop peer") 23 | if self.ipv6_multi_hop and not ipv6_next_hop: 24 | raise LookupError("Unable to determine IPv6 next hop for multihop peer") 25 | 26 | return { 27 | "bgp_neighbors": [neighbor._asdict() for neighbor in self.bgp_neighbors], 28 | "meta": { 29 | "router_id": self.router_id, 30 | "family": self.family, 31 | "ipv4_next_hop": ipv4_next_hop if self.ipv4_multi_hop else None, 32 | "ipv6_next_hop": ipv6_next_hop if self.ipv6_multi_hop else None, 33 | }, 34 | } 35 | 36 | def render_config(self, data: Dict[str, Any], filename: str) -> str: 37 | script_dir = os.path.dirname(__file__) 38 | search_dir = os.path.join(script_dir, "templates") 39 | loader = FileSystemLoader(searchpath=search_dir) 40 | env = Environment(loader=loader) 41 | 42 | try: 43 | template = env.get_template(filename) 44 | except TemplateNotFound as e: 45 | raise TemplateNotFound( 46 | "Failed to locate bird's configuration template {}.".format(e.message) 47 | ) 48 | 49 | return template.render(data=data) 50 | -------------------------------------------------------------------------------- /routers/bird/templates/bird.conf.j2: -------------------------------------------------------------------------------- 1 | filter packet_bgp { 2 | # the IP range(s) to announce via BGP from this machine 3 | # these IP addresses need to be bound to the lo interface 4 | # to be reachable; the default behavior is to accept all 5 | # prefixes bound to interface lo 6 | # if net = A.B.C.D/32 then accept; 7 | accept; 8 | } 9 | 10 | router id {{ data.meta.router_id }}; 11 | 12 | protocol direct { 13 | interface "lo"; # Restrict network interfaces BIRD works with 14 | } 15 | 16 | protocol kernel { 17 | persist; # Don't remove routes on bird shutdown 18 | scan time 20; # Scan kernel routing table every 20 seconds 19 | import all; # Default is import all 20 | export all; # Default is export none 21 | } 22 | 23 | {% if data.meta.ipv4_next_hop and data.meta.family == 4 -%} 24 | protocol static { 25 | {%- for group in data.bgp_neighbors %} 26 | {%- if group.address_family == data.meta.family %} 27 | {%- for neighbor in group.peer_ips %} 28 | route {{ neighbor }}/32 via {{ data.meta.ipv4_next_hop }}; 29 | {%- endfor %} 30 | {%- endif %} 31 | {%- endfor %} 32 | } 33 | {%- elif data.meta.ipv6_next_hop and data.meta.family == 6 -%} 34 | protocol static { 35 | {%- for group in data.bgp_neighbors %} 36 | {%- if group.address_family == data.meta.family %} 37 | {%- for neighbor in group.peer_ips %} 38 | route {{ neighbor }}/128 via {{ data.meta.ipv6_next_hop }}; 39 | {%- endfor %} 40 | {%- endif %} 41 | {%- endfor %} 42 | } 43 | {%- endif %} 44 | 45 | # This pseudo-protocol watches all interface up/down events. 46 | protocol device { 47 | scan time 10; # Scan interfaces every 10 seconds 48 | } 49 | {% for group in data.bgp_neighbors %} 50 | {%- if group.address_family == data.meta.family %} 51 | {%- for neighbor in group.peer_ips %} 52 | protocol bgp neighbor_v{{ group.address_family }}_{{ loop.index }} { 53 | export filter packet_bgp; 54 | local as {{ group.customer_as }}; 55 | {%- if group.multihop %} 56 | multihop 5; 57 | {%- endif %} 58 | neighbor {{ neighbor }} as {{ group.peer_as }}; 59 | {%- if group.md5_enabled %} 60 | password "{{ group.md5_password }}"; 61 | {%- endif %} 62 | } 63 | {% endfor %} 64 | {%- endif %} 65 | {%- endfor %} 66 | -------------------------------------------------------------------------------- /routers/bird/tests/data/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | 3 | INVALID_RESPONSES: List[Any] = [ 4 | "404 Error", 5 | {"errors": ["Not found"]}, 6 | { 7 | "bgp_neighbors": [ 8 | { 9 | "md5_enabled": True, 10 | "md5_password": "ValidPassword123", 11 | "multihop": True, 12 | "peer_as": 65530, 13 | "peer_ips": [ 14 | "fc00:0000:0000:0000:0000:0000:0000:000e", 15 | "fc00:0000:0000:0000:0000:0000:0000:000f", 16 | ], 17 | "routes_in": [ 18 | {"exact": False, "route": "2604:1380:1:7400::/56"}, 19 | {"exact": False, "route": "2604:1380:4111:2300::/56"}, 20 | ], 21 | "routes_out": [], 22 | } 23 | ] 24 | }, 25 | { 26 | "bgp_neighbors": [ 27 | { 28 | "address_family": 4, 29 | "customer_as": 65000, 30 | "customer_ip": "10.99.182.129", 31 | "md5_enabled": True, 32 | "md5_password": "ValidPassword123", 33 | "multihop": False, 34 | "peer_as": 65530, 35 | "peer_ips": [], 36 | "routes_in": [ 37 | {"exact": False, "route": "10.1.0.0/31"}, 38 | {"exact": False, "route": "10.2.0.0/29"}, 39 | ], 40 | "routes_out": [], 41 | }, 42 | { 43 | "address_family": 6, 44 | "customer_as": 65000, 45 | "customer_ip": "2604:1380:4111:2300::1", 46 | "md5_enabled": True, 47 | "md5_password": "ValidPassword123", 48 | "multihop": False, 49 | "peer_as": 65530, 50 | "peer_ips": [], 51 | "routes_in": [ 52 | {"exact": False, "route": "2604:1380:1:7400::/56"}, 53 | {"exact": False, "route": "2604:1380:4111:2300::/56"}, 54 | ], 55 | "routes_out": [], 56 | }, 57 | ] 58 | }, 59 | ] 60 | -------------------------------------------------------------------------------- /routers/frr/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Any, Dict, List 3 | 4 | from jinja2 import Environment, FileSystemLoader 5 | from jinja2.exceptions import TemplateNotFound 6 | from routers import Router 7 | 8 | 9 | class FRR(Router): 10 | def __init__(self, **kwargs: Any) -> None: 11 | super().__init__(**kwargs) 12 | if not self.bgp_neighbors: 13 | raise ValueError("At least one bgp neighbor is required") 14 | self.config = self.render_config(self.build_config(), "frr.conf.j2").strip() 15 | 16 | def build_config(self) -> Dict[str, Any]: 17 | ipv4_next_hop, ipv6_next_hop = self.multi_hop_gateway 18 | 19 | if self.ipv4_multi_hop and not ipv4_next_hop: 20 | raise LookupError("Unable to determine IPv4 next hop for multihop peer") 21 | if self.ipv6_multi_hop and not ipv6_next_hop: 22 | raise LookupError("Unable to determine IPv6 next hop for multihop peer") 23 | 24 | bgp_neighbors_per_asn: Dict[int, List[Any]] = {} 25 | for neighbor in self.bgp_neighbors: 26 | if not neighbor.peer_ips: 27 | raise ValueError("At least one peer ip per bgp group is required") 28 | if neighbor.customer_as not in bgp_neighbors_per_asn: 29 | bgp_neighbors_per_asn[neighbor.customer_as] = [] 30 | bgp_neighbors_per_asn[neighbor.customer_as].append(neighbor._asdict()) 31 | 32 | return { 33 | "bgp_neighbors": bgp_neighbors_per_asn, 34 | "meta": {"ipv4_next_hop": ipv4_next_hop, "ipv6_next_hop": ipv6_next_hop}, 35 | } 36 | 37 | def render_config(self, data: Dict[str, Any], filename: str) -> str: 38 | script_dir = os.path.dirname(__file__) 39 | search_dir = os.path.join(script_dir, "templates") 40 | loader = FileSystemLoader(searchpath=search_dir) 41 | env = Environment(loader=loader) 42 | 43 | try: 44 | template = env.get_template(filename) 45 | except TemplateNotFound as e: 46 | raise TemplateNotFound( 47 | "Failed to locate frr's configuration template {}.".format(e.message) 48 | ) 49 | 50 | return template.render(data=data) 51 | -------------------------------------------------------------------------------- /routers/bird/tests/test_bird.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | import pytest 4 | from routers.bird import Bird 5 | from routers.bird.tests.data import INVALID_RESPONSES 6 | from routers.test_data import initialize 7 | 8 | VALID_RESPONSES = initialize() 9 | 10 | 11 | @pytest.mark.parametrize( 12 | "test_input,expected,test_type", 13 | [ 14 | (VALID_RESPONSES[0], {4: 1, 6: 0}, "valid"), 15 | (VALID_RESPONSES[1], {4: 0, 6: 1}, "valid"), 16 | (VALID_RESPONSES[2], {4: 1, 6: 1}, "valid"), 17 | (VALID_RESPONSES[3], {4: 2, 6: 0}, "valid"), 18 | (VALID_RESPONSES[4], {4: 0, 6: 2}, "valid"), 19 | (VALID_RESPONSES[5], {4: 2, 6: 2}, "valid"), 20 | (INVALID_RESPONSES[0], TypeError, "invalid"), # Non-json response 21 | ( 22 | INVALID_RESPONSES[1], 23 | LookupError, 24 | "invalid", 25 | ), # Json response without bgp data 26 | ( 27 | INVALID_RESPONSES[2], 28 | TypeError, 29 | "invalid", 30 | ), # Json response with missing fields in bgp_neighbors 31 | ( 32 | INVALID_RESPONSES[3], 33 | LookupError, 34 | "invalid", 35 | ), # Json response with no peering sessions in bgp_neighbors 36 | ], 37 | ) 38 | def test_Bird(test_input: Dict[str, Any], expected: Any, test_type: int) -> None: 39 | if test_type == "valid": 40 | bird = Bird(**test_input) 41 | if len(bird.bgp_neighbors) == 1: 42 | neighbor = bird.bgp_neighbors.pop() 43 | if neighbor.address_family == 4: 44 | assert expected[4] == len(neighbor.peer_ips) 45 | assert expected[6] == 0 46 | if neighbor.address_family == 6: 47 | assert expected[4] == 0 48 | assert expected[6] == len(neighbor.peer_ips) 49 | else: 50 | for j in range(len(bird.bgp_neighbors)): 51 | assert expected[bird.bgp_neighbors[j].address_family] == len( 52 | bird.bgp_neighbors[j].peer_ips 53 | ) 54 | else: 55 | try: 56 | bird = Bird(**test_input) 57 | assert False 58 | except Exception as e: 59 | assert type(e) == expected 60 | -------------------------------------------------------------------------------- /routers/frr/tests/test_frr.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | import pytest 4 | from routers.frr import FRR 5 | from routers.frr.tests.data import INVALID_RESPONSES 6 | from routers.test_data import initialize 7 | 8 | VALID_RESPONSES = initialize() 9 | 10 | 11 | @pytest.mark.parametrize( 12 | "test_input,expected,test_type", 13 | [ 14 | (VALID_RESPONSES[0], {4: 1, 6: 0}, "valid"), 15 | (VALID_RESPONSES[1], {4: 0, 6: 1}, "valid"), 16 | (VALID_RESPONSES[2], {4: 1, 6: 1}, "valid"), 17 | (VALID_RESPONSES[3], {4: 2, 6: 0}, "valid"), 18 | (VALID_RESPONSES[4], {4: 0, 6: 2}, "valid"), 19 | (VALID_RESPONSES[5], {4: 2, 6: 2}, "valid"), 20 | (INVALID_RESPONSES[0], TypeError, "invalid"), # Non-json response 21 | (INVALID_RESPONSES[1], ValueError, "invalid"), # Json response without bgp data 22 | ( 23 | INVALID_RESPONSES[2], 24 | TypeError, 25 | "invalid", 26 | ), # Json response with missing fields in bgp_neighbors 27 | ( 28 | INVALID_RESPONSES[3], 29 | ValueError, 30 | "invalid", 31 | ), # Json response with no peering sessions in bgp_neighbors 32 | ( 33 | INVALID_RESPONSES[4], 34 | LookupError, 35 | "invalid", 36 | ), # Json response with multihop peering sessions but no usable management ip 37 | ], 38 | ) 39 | def test_FRR(test_input: Dict[str, Any], expected: Any, test_type: int) -> None: 40 | if test_type == "valid": 41 | frr = FRR(**test_input) 42 | if len(frr.bgp_neighbors) == 1: 43 | neighbor = frr.bgp_neighbors.pop() 44 | if neighbor.address_family == 4: 45 | assert expected[4] == len(neighbor.peer_ips) 46 | assert expected[6] == 0 47 | if neighbor.address_family == 6: 48 | assert expected[4] == 0 49 | assert expected[6] == len(neighbor.peer_ips) 50 | else: 51 | for j in range(len(frr.bgp_neighbors)): 52 | assert expected[frr.bgp_neighbors[j].address_family] == len( 53 | frr.bgp_neighbors[j].peer_ips 54 | ) 55 | else: 56 | try: 57 | frr = FRR(**test_input) 58 | assert False 59 | except Exception as e: 60 | assert type(e) == expected 61 | -------------------------------------------------------------------------------- /routers/frr/daemons: -------------------------------------------------------------------------------- 1 | # This file tells the frr package which daemons to start. 2 | # 3 | # Sample configurations for these daemons can be found in 4 | # /usr/share/doc/frr/examples/. 5 | # 6 | # ATTENTION: 7 | # 8 | # When activating a daemon for the first time, a config file, even if it is 9 | # empty, has to be present *and* be owned by the user and group "frr", else 10 | # the daemon will not be started by /etc/init.d/frr. The permissions should 11 | # be u=rw,g=r,o=. 12 | # When using "vtysh" such a config file is also needed. It should be owned by 13 | # group "frrvty" and set to ug=rw,o= though. Check /etc/pam.d/frr, too. 14 | # 15 | # The watchfrr and zebra daemons are always started. 16 | # 17 | bgpd=yes 18 | ospfd=no 19 | ospf6d=no 20 | ripd=no 21 | ripngd=no 22 | isisd=no 23 | pimd=no 24 | ldpd=no 25 | nhrpd=no 26 | eigrpd=no 27 | babeld=no 28 | sharpd=no 29 | pbrd=no 30 | bfdd=no 31 | fabricd=no 32 | vrrpd=no 33 | 34 | # 35 | # If this option is set the /etc/init.d/frr script automatically loads 36 | # the config via "vtysh -b" when the servers are started. 37 | # Check /etc/pam.d/frr if you intend to use "vtysh"! 38 | # 39 | vtysh_enable=yes 40 | zebra_options=" -A 127.0.0.1 -s 90000000" 41 | bgpd_options=" -A 127.0.0.1" 42 | ospfd_options=" -A 127.0.0.1" 43 | ospf6d_options=" -A ::1" 44 | ripd_options=" -A 127.0.0.1" 45 | ripngd_options=" -A ::1" 46 | isisd_options=" -A 127.0.0.1" 47 | pimd_options=" -A 127.0.0.1" 48 | ldpd_options=" -A 127.0.0.1" 49 | nhrpd_options=" -A 127.0.0.1" 50 | eigrpd_options=" -A 127.0.0.1" 51 | babeld_options=" -A 127.0.0.1" 52 | sharpd_options=" -A 127.0.0.1" 53 | pbrd_options=" -A 127.0.0.1" 54 | staticd_options="-A 127.0.0.1" 55 | bfdd_options=" -A 127.0.0.1" 56 | fabricd_options="-A 127.0.0.1" 57 | vrrpd_options=" -A 127.0.0.1" 58 | 59 | # 60 | # This is the maximum number of FD's that will be available. 61 | # Upon startup this is read by the control files and ulimit 62 | # is called. Uncomment and use a reasonable value for your 63 | # setup if you are expecting a large number of peers in 64 | # say BGP. 65 | #MAX_FDS=1024 66 | 67 | # The list of daemons to watch is automatically generated by the init script. 68 | #watchfrr_options="" 69 | 70 | # for debugging purposes, you can specify a "wrap" command to start instead 71 | # of starting the daemon directly, e.g. to use valgrind on ospfd: 72 | # ospfd_wrap="/usr/bin/valgrind" 73 | # or you can use "all_wrap" for all daemons, e.g. to use perf record: 74 | # all_wrap="/usr/bin/perf record --call-graph -" 75 | # the normal daemon command is added to this at the end. 76 | -------------------------------------------------------------------------------- /routers/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, NamedTuple, Optional, Tuple 2 | 3 | import jmespath 4 | 5 | BgpNeighbor = NamedTuple( 6 | "BgpNeighbor", 7 | [ 8 | ("address_family", int), 9 | ("customer_as", int), 10 | ("customer_ip", str), 11 | ("md5_enabled", bool), 12 | ("md5_password", str), 13 | ("multihop", bool), 14 | ("peer_as", int), 15 | ("peer_ips", List[str]), 16 | ("routes_in", List[Dict[str, Any]]), 17 | ("routes_out", List[Dict[str, Any]]), 18 | ], 19 | ) 20 | 21 | 22 | class Router: 23 | def __init__(self, **kwargs: Any) -> None: 24 | self.bgp_neighbors: List[BgpNeighbor] = [] 25 | self.v4_peer_count = 0 26 | self.v6_peer_count = 0 27 | self.bgp_neighbors = [] 28 | self.bgp_neighbor_dicts = [] 29 | if "bgp_neighbors" in kwargs: 30 | self.bgp_neighbor_dicts = kwargs["bgp_neighbors"] 31 | for neighbor in kwargs["bgp_neighbors"]: 32 | self.bgp_neighbors.append(BgpNeighbor(**neighbor)) 33 | if neighbor["address_family"] == 4: 34 | self.v4_peer_count = len(neighbor["peer_ips"]) 35 | elif neighbor["address_family"] == 6: 36 | self.v6_peer_count = len(neighbor["peer_ips"]) 37 | 38 | try: 39 | self.ip_addresses = kwargs["network"]["addresses"] 40 | except KeyError: 41 | self.ip_addresses = [] 42 | 43 | try: 44 | self.ipv4_multi_hop = bool( 45 | jmespath.search( 46 | "[?address_family == `4`].multihop | [0]", self.bgp_neighbor_dicts 47 | ) 48 | ) 49 | self.ipv6_multi_hop = bool( 50 | jmespath.search( 51 | "[?address_family == `6`].multihop | [0]", self.bgp_neighbor_dicts 52 | ) 53 | ) 54 | except Exception as e: 55 | raise LookupError( 56 | "Unable to parse multihop attribute from bgp_neighbors: {}.".format(e) 57 | ) 58 | 59 | @property 60 | def router_id(self) -> Optional[str]: 61 | router_id = None 62 | for address in self.ip_addresses: 63 | if ( 64 | address["address_family"] == 4 65 | and not address["public"] 66 | and address["management"] 67 | ): 68 | router_id = address["address"] 69 | break 70 | 71 | return router_id 72 | 73 | @property 74 | def multi_hop_gateway(self) -> Tuple[Optional[str], Optional[str]]: 75 | try: 76 | ipv4_next_hop = jmespath.search( 77 | "[?address_family == `4` && public && management].gateway | [0]", 78 | self.ip_addresses, 79 | ) 80 | ipv6_next_hop = jmespath.search( 81 | "[?address_family == `6` && public && management].gateway | [0]", 82 | self.ip_addresses, 83 | ) 84 | except Exception as e: 85 | raise LookupError( 86 | "Unable to parse static route next hop(s) from instance ip_addresses: {}.".format( 87 | e 88 | ) 89 | ) 90 | 91 | return (ipv4_next_hop, ipv6_next_hop) 92 | -------------------------------------------------------------------------------- /routers/frr/tests/data/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | 3 | INVALID_RESPONSES: List[Any] = [ 4 | "404 Error", 5 | {"errors": ["Not found"]}, 6 | { 7 | "bgp_neighbors": [ 8 | { 9 | "md5_enabled": True, 10 | "md5_password": "ValidPassword123", 11 | "multihop": True, 12 | "peer_as": 65530, 13 | "peer_ips": [ 14 | "fc00:0000:0000:0000:0000:0000:0000:000e", 15 | "fc00:0000:0000:0000:0000:0000:0000:000f", 16 | ], 17 | "routes_in": [ 18 | {"exact": False, "route": "2604:1380:1:7400::/56"}, 19 | {"exact": False, "route": "2604:1380:4111:2300::/56"}, 20 | ], 21 | "routes_out": [], 22 | } 23 | ] 24 | }, 25 | { 26 | "bgp_neighbors": [ 27 | { 28 | "address_family": 4, 29 | "customer_as": 65000, 30 | "customer_ip": "10.99.182.129", 31 | "md5_enabled": True, 32 | "md5_password": "ValidPassword123", 33 | "multihop": False, 34 | "peer_as": 65530, 35 | "peer_ips": [], 36 | "routes_in": [ 37 | {"exact": False, "route": "10.1.0.0/31"}, 38 | {"exact": False, "route": "10.2.0.0/29"}, 39 | ], 40 | "routes_out": [], 41 | }, 42 | { 43 | "address_family": 6, 44 | "customer_as": 65000, 45 | "customer_ip": "2604:1380:4111:2300::1", 46 | "md5_enabled": True, 47 | "md5_password": "ValidPassword123", 48 | "multihop": False, 49 | "peer_as": 65530, 50 | "peer_ips": [], 51 | "routes_in": [ 52 | {"exact": False, "route": "2604:1380:1:7400::/56"}, 53 | {"exact": False, "route": "2604:1380:4111:2300::/56"}, 54 | ], 55 | "routes_out": [], 56 | }, 57 | ] 58 | }, 59 | { 60 | "bgp_neighbors": [ 61 | { 62 | "address_family": 4, 63 | "customer_as": 65000, 64 | "customer_ip": "10.99.182.129", 65 | "md5_enabled": True, 66 | "md5_password": "ValidPassword123", 67 | "multihop": True, 68 | "peer_as": 65530, 69 | "peer_ips": ["169.254.255.1", "169.254.255.2"], 70 | "routes_in": [ 71 | {"exact": False, "route": "10.1.0.0/31"}, 72 | {"exact": False, "route": "10.2.0.0/29"}, 73 | ], 74 | "routes_out": [], 75 | }, 76 | { 77 | "address_family": 6, 78 | "customer_as": 65000, 79 | "customer_ip": "2604:1380:4111:2300::1", 80 | "md5_enabled": True, 81 | "md5_password": "ValidPassword123", 82 | "multihop": True, 83 | "peer_as": 65530, 84 | "peer_ips": [ 85 | "fc00:0000:0000:0000:0000:0000:0000:000e", 86 | "fc00:0000:0000:0000:0000:0000:0000:000f", 87 | ], 88 | "routes_in": [ 89 | {"exact": False, "route": "2604:1380:1:7400::/56"}, 90 | {"exact": False, "route": "2604:1380:4111:2300::/56"}, 91 | ], 92 | "routes_out": [], 93 | }, 94 | ], 95 | "network": { 96 | "addresses": [ 97 | { 98 | "address": "147.75.65.31", 99 | "address_family": 4, 100 | "cidr": 31, 101 | "customdata": {}, 102 | "enabled": True, 103 | "gateway": "147.75.65.30", 104 | "global_ip": None, 105 | "manageable": True, 106 | "management": False, 107 | "netmask": "255.255.255.254", 108 | "network": "147.75.65.30", 109 | "public": True, 110 | }, 111 | { 112 | "address": "2604:1380:1:5f00::1", 113 | "address_family": 6, 114 | "cidr": 127, 115 | "customdata": {}, 116 | "enabled": True, 117 | "gateway": "2604:1380:1:5f00::", 118 | "global_ip": None, 119 | "manageable": True, 120 | "management": False, 121 | "netmask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe", 122 | "network": "2604:1380:1:5f00::", 123 | "public": True, 124 | }, 125 | { 126 | "address": "10.99.182.129", 127 | "address_family": 4, 128 | "cidr": 31, 129 | "customdata": {}, 130 | "enabled": True, 131 | "gateway": "10.99.182.128", 132 | "global_ip": None, 133 | "manageable": True, 134 | "management": False, 135 | "netmask": "255.255.255.254", 136 | "network": "10.99.182.128", 137 | "public": False, 138 | }, 139 | { 140 | "address": "10.99.182.254", 141 | "address_family": 4, 142 | "cidr": 32, 143 | "customdata": {}, 144 | "enabled": True, 145 | "gateway": "10.99.182.254", 146 | "global_ip": None, 147 | "manageable": True, 148 | "management": False, 149 | "netmask": "255.255.255.255", 150 | "network": "10.99.182.254", 151 | "public": False, 152 | }, 153 | ] 154 | }, 155 | }, 156 | ] 157 | -------------------------------------------------------------------------------- /routers/test_data/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List 2 | 3 | IP_ADDRESSES: List[Dict[str, Any]] = [ 4 | { 5 | "address": "147.75.65.31", 6 | "address_family": 4, 7 | "cidr": 31, 8 | "customdata": {}, 9 | "enabled": True, 10 | "gateway": "147.75.65.30", 11 | "global_ip": None, 12 | "manageable": True, 13 | "management": True, 14 | "netmask": "255.255.255.254", 15 | "network": "147.75.65.30", 16 | "public": True, 17 | }, 18 | { 19 | "address": "2604:1380:1:5f00::1", 20 | "address_family": 6, 21 | "cidr": 127, 22 | "customdata": {}, 23 | "enabled": True, 24 | "gateway": "2604:1380:1:5f00::", 25 | "global_ip": None, 26 | "manageable": True, 27 | "management": True, 28 | "netmask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe", 29 | "network": "2604:1380:1:5f00::", 30 | "public": True, 31 | }, 32 | { 33 | "address": "10.99.182.129", 34 | "address_family": 4, 35 | "cidr": 31, 36 | "customdata": {}, 37 | "enabled": True, 38 | "gateway": "10.99.182.128", 39 | "global_ip": None, 40 | "manageable": True, 41 | "management": True, 42 | "netmask": "255.255.255.254", 43 | "network": "10.99.182.128", 44 | "public": False, 45 | }, 46 | { 47 | "address": "10.99.182.254", 48 | "address_family": 4, 49 | "cidr": 32, 50 | "customdata": {}, 51 | "enabled": True, 52 | "gateway": "10.99.182.254", 53 | "global_ip": None, 54 | "manageable": True, 55 | "management": False, 56 | "netmask": "255.255.255.255", 57 | "network": "10.99.182.254", 58 | "public": False, 59 | }, 60 | ] 61 | 62 | VALID_RESPONSES: List[Dict[str, Any]] = [ 63 | { 64 | "bgp_neighbors": [ 65 | { 66 | "address_family": 4, 67 | "customer_as": 65000, 68 | "customer_ip": "10.99.182.129", 69 | "md5_enabled": False, 70 | "md5_password": None, 71 | "multihop": False, 72 | "peer_as": 65530, 73 | "peer_ips": ["169.254.255.1"], 74 | "routes_in": [ 75 | {"exact": False, "route": "10.1.0.0/31"}, 76 | {"exact": False, "route": "10.2.0.0/29"}, 77 | ], 78 | "routes_out": [], 79 | } 80 | ] 81 | }, 82 | { 83 | "bgp_neighbors": [ 84 | { 85 | "address_family": 6, 86 | "customer_as": 65000, 87 | "customer_ip": "2604:1380:4111:2300::1", 88 | "md5_enabled": True, 89 | "md5_password": "ValidPassword123", 90 | "multihop": False, 91 | "peer_as": 65530, 92 | "peer_ips": ["fc00:0000:0000:0000:0000:0000:0000:000e"], 93 | "routes_in": [ 94 | {"exact": False, "route": "2604:1380:1:7400::/56"}, 95 | {"exact": False, "route": "2604:1380:4111:2300::/56"}, 96 | ], 97 | "routes_out": [], 98 | } 99 | ] 100 | }, 101 | { 102 | "bgp_neighbors": [ 103 | { 104 | "address_family": 4, 105 | "customer_as": 65000, 106 | "customer_ip": "10.99.182.129", 107 | "md5_enabled": True, 108 | "md5_password": "ValidPassword123", 109 | "multihop": False, 110 | "peer_as": 65530, 111 | "peer_ips": ["169.254.255.1"], 112 | "routes_in": [ 113 | {"exact": False, "route": "10.1.0.0/31"}, 114 | {"exact": False, "route": "10.2.0.0/29"}, 115 | ], 116 | "routes_out": [], 117 | }, 118 | { 119 | "address_family": 6, 120 | "customer_as": 65000, 121 | "customer_ip": "2604:1380:4111:2300::1", 122 | "md5_enabled": True, 123 | "md5_password": "ValidPassword123", 124 | "multihop": False, 125 | "peer_as": 65530, 126 | "peer_ips": ["fc00:0000:0000:0000:0000:0000:0000:000e"], 127 | "routes_in": [ 128 | {"exact": False, "route": "2604:1380:1:7400::/56"}, 129 | {"exact": False, "route": "2604:1380:4111:2300::/56"}, 130 | ], 131 | "routes_out": [], 132 | }, 133 | ] 134 | }, 135 | { 136 | "bgp_neighbors": [ 137 | { 138 | "address_family": 4, 139 | "customer_as": 65000, 140 | "customer_ip": "10.99.182.129", 141 | "md5_enabled": True, 142 | "md5_password": "ValidPassword123", 143 | "multihop": True, 144 | "peer_as": 65530, 145 | "peer_ips": ["169.254.255.1", "169.254.255.2"], 146 | "routes_in": [ 147 | {"exact": False, "route": "10.1.0.0/31"}, 148 | {"exact": False, "route": "10.2.0.0/29"}, 149 | ], 150 | "routes_out": [], 151 | } 152 | ] 153 | }, 154 | { 155 | "bgp_neighbors": [ 156 | { 157 | "address_family": 6, 158 | "customer_as": 65000, 159 | "customer_ip": "2604:1380:4111:2300::1", 160 | "md5_enabled": True, 161 | "md5_password": "ValidPassword123", 162 | "multihop": True, 163 | "peer_as": 65530, 164 | "peer_ips": [ 165 | "fc00:0000:0000:0000:0000:0000:0000:000e", 166 | "fc00:0000:0000:0000:0000:0000:0000:000f", 167 | ], 168 | "routes_in": [ 169 | {"exact": False, "route": "2604:1380:1:7400::/56"}, 170 | {"exact": False, "route": "2604:1380:4111:2300::/56"}, 171 | ], 172 | "routes_out": [], 173 | } 174 | ] 175 | }, 176 | { 177 | "bgp_neighbors": [ 178 | { 179 | "address_family": 4, 180 | "customer_as": 65000, 181 | "customer_ip": "10.99.182.129", 182 | "md5_enabled": True, 183 | "md5_password": "ValidPassword123", 184 | "multihop": True, 185 | "peer_as": 65530, 186 | "peer_ips": ["169.254.255.1", "169.254.255.2"], 187 | "routes_in": [ 188 | {"exact": False, "route": "10.1.0.0/31"}, 189 | {"exact": False, "route": "10.2.0.0/29"}, 190 | ], 191 | "routes_out": [], 192 | }, 193 | { 194 | "address_family": 6, 195 | "customer_as": 65000, 196 | "customer_ip": "2604:1380:4111:2300::1", 197 | "md5_enabled": True, 198 | "md5_password": "ValidPassword123", 199 | "multihop": True, 200 | "peer_as": 65530, 201 | "peer_ips": [ 202 | "fc00:0000:0000:0000:0000:0000:0000:000e", 203 | "fc00:0000:0000:0000:0000:0000:0000:000f", 204 | ], 205 | "routes_in": [ 206 | {"exact": False, "route": "2604:1380:1:7400::/56"}, 207 | {"exact": False, "route": "2604:1380:4111:2300::/56"}, 208 | ], 209 | "routes_out": [], 210 | }, 211 | ] 212 | }, 213 | ] 214 | 215 | 216 | def initialize() -> List[Dict[str, str]]: 217 | for i in range(len(VALID_RESPONSES)): 218 | VALID_RESPONSES[i]["network"] = {"addresses": IP_ADDRESSES} 219 | 220 | return VALID_RESPONSES 221 | -------------------------------------------------------------------------------- /routers/bird/README.md: -------------------------------------------------------------------------------- 1 | ## Bird 2 | 3 | Bird is an open source routing daemon for Unix-like systems. It can be used to establish bgp sessions between client instances and the Packet network. An elastic IP address can simply be announced via Bird from the instance that is currently using it, thereby making it easy to freely move the IP from one instance to another within the same project. 4 | 5 | ### Requirements 6 | 7 | * A BGP enabled project and instance 8 | * An elastic IP configured on interface lo 9 | 10 | The former can be accomplished via the Packet client portal while the latter can be done via the following two commands (this applies to Ubuntu; network configuration will typically vary by distro): 11 | 12 | ```bash 13 | echo 'auto lo:0 14 | iface lo:0 inet static 15 | address 10.99.182.254 16 | netmask 255.255.255.255' >> /etc/network/interfaces 17 | ``` 18 | 19 | In the above command, you will replace `10.99.182.254` with your own elastic IP. Then simply bring up interface `lo:0`: 20 | 21 | ```bash 22 | ifup lo:0 23 | ``` 24 | 25 | ### Configuration 26 | 27 | #### Method 1: Bird on Baremetal 28 | 29 | First install system dependencies. On Ubuntu 18.04 this looks like: 30 | 31 | ```bash 32 | apt -y update && apt -y install python3.6 python3-pip git bird 33 | ``` 34 | 35 | Now clone the repo and install remaining python dependencies: 36 | 37 | ```bash 38 | cd /opt 39 | git clone https://github.com/packethost/network-helpers.git 40 | cd network-helpers 41 | pip3 install jmespath 42 | pip3 install -e . 43 | ``` 44 | 45 | Now use the `configure.py` script to configure bird: 46 | 47 | ``` 48 | ./configure.py -r bird | tee /etc/bird/bird.conf 49 | filter packet_bgp { 50 | # the IP range(s) to announce via BGP from this machine 51 | # these IP addresses need to be bound to the lo interface 52 | # to be reachable; the default behavior is to accept all 53 | # prefixes bound to interface lo 54 | # if net = A.B.C.D/32 then accept; 55 | accept; 56 | } 57 | 58 | router id 10.99.182.129; 59 | 60 | protocol direct { 61 | interface "lo"; # Restrict network interfaces BIRD works with 62 | } 63 | 64 | protocol kernel { 65 | persist; # Don't remove routes on bird shutdown 66 | scan time 20; # Scan kernel routing table every 20 seconds 67 | import all; # Default is import all 68 | export all; # Default is export none 69 | } 70 | 71 | # This pseudo-protocol watches all interface up/down events. 72 | protocol device { 73 | scan time 10; # Scan interfaces every 10 seconds 74 | } 75 | 76 | protocol bgp neighbor_v4_1 { 77 | export filter packet_bgp; 78 | local as 65000; 79 | neighbor 10.99.182.128 as 65530; 80 | password "somepassword"; 81 | } 82 | ``` 83 | 84 | Check that the config looks correct, then restart bird: 85 | 86 | ```bash 87 | systemctl restart bird 88 | ``` 89 | 90 | And verify: 91 | 92 | ```bash 93 | birdc 94 | ``` 95 | ``` 96 | bird> show protocols all neighbor_v4_1 97 | name proto table state since info 98 | neighbor_v4_1 BGP master up 15:20:31 Established 99 | Preference: 100 100 | Input filter: ACCEPT 101 | Output filter: packet_bgp 102 | Routes: 0 imported, 1 exported, 0 preferred 103 | Route change stats: received rejected filtered ignored accepted 104 | Import updates: 0 0 0 0 0 105 | Import withdraws: 0 0 --- 0 0 106 | Export updates: 1 0 0 --- 1 107 | Export withdraws: 0 --- --- --- 0 108 | BGP state: Established 109 | Neighbor address: 10.99.182.128 110 | Neighbor AS: 65530 111 | Neighbor ID: 147.75.36.73 112 | Neighbor caps: refresh restart-aware AS4 113 | Session: external AS4 114 | Source address: 10.99.182.129 115 | Hold timer: 66/90 116 | Keepalive timer: 18/30 117 | ``` 118 | 119 | In this case we only have a single elastic IP bound to interface lo, and we see the prefix is being exported and accepted so we are done. 120 | 121 | **Note:** Bird uses separate daemons for IPv4 and IPv6 routing. To configure bgp over IPv6 using bird, the process will be the same as above, except that the configuration command you will use will be: 122 | 123 | ```bash 124 | ./configure.py -r bird6 | tee /etc/bird/bird6.conf 125 | ``` 126 | 127 | Then: 128 | 129 | ```bash 130 | systemctl restart bird6 131 | ``` 132 | 133 | #### Method 2: Bird via Docker 134 | 135 | Using your OS's package management utility, install docker, docker-compose and git if not already installed. On Ubuntu 18.04 this looks like: 136 | 137 | ```bash 138 | apt -y update && apt -y install docker docker-compose git 139 | systemctl enable docker && systemctl start docker 140 | ``` 141 | 142 | Clone the repo: 143 | 144 | ```bash 145 | cd /opt 146 | git clone https://github.com/packethost/network-helpers.git 147 | ``` 148 | 149 | Build the image: 150 | 151 | ```bash 152 | cd network-helpers 153 | docker build -f routers/bird/Dockerfile -t local/bird:latest . 154 | ``` 155 | 156 | Up the container: 157 | 158 | ```bash 159 | cd routers/bird 160 | docker-compose up -d 161 | ``` 162 | 163 | To verify that the bird service was started cleanly and correctly, we can view the container logs: 164 | 165 | ``` 166 | docker logs $(docker ps | awk '$2 == "local/bird:latest" {print $1}') 167 | + /opt/bgp/configure.py -r bird 168 | + tee /etc/bird/bird.conf 169 | filter packet_bgp { 170 | # the IP range(s) to announce via BGP from this machine 171 | # these IP addresses need to be bound to the lo interface 172 | # to be reachable; the default behavior is to accept all 173 | # prefixes bound to interface lo 174 | # if net = A.B.C.D/32 then accept; 175 | accept; 176 | } 177 | 178 | router id 10.99.182.129; 179 | 180 | protocol direct { 181 | interface "lo"; # Restrict network interfaces BIRD works with 182 | } 183 | 184 | protocol kernel { 185 | persist; # Don't remove routes on bird shutdown 186 | scan time 20; # Scan kernel routing table every 20 seconds 187 | import all; # Default is import all 188 | export all; # Default is export none 189 | } 190 | 191 | # This pseudo-protocol watches all interface up/down events. 192 | protocol device { 193 | scan time 10; # Scan interfaces every 10 seconds 194 | } 195 | 196 | protocol bgp neighbor_v4_1 { 197 | export filter packet_bgp; 198 | local as 65000; 199 | neighbor 10.99.182.128 as 65530; 200 | password "somepassword"; 201 | } 202 | + '[' 0 == 0 ']' 203 | + echo 204 | + cat 205 | + supervisord -c /opt/bgp/routers/bird/supervisord.conf 206 | 2020-04-09 13:44:27,263 WARN For [program:bird], AUTO logging used for stderr_logfile without rollover, set maxbytes > 0 to avoid filling up filesystem unintentionally 207 | 2020-04-09 13:44:27,263 INFO Set uid to user 0 succeeded 208 | 2020-04-09 13:44:27,265 INFO supervisord started with pid 12 209 | 2020-04-09 13:44:28,270 INFO spawned: 'bird' with pid 16 210 | 2020-04-09 13:44:29,274 INFO success: bird entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) 211 | ``` 212 | 213 | And verify: 214 | 215 | ```bash 216 | docker exec -it $(docker ps | awk '$2 == "local/bird:latest" {print $1}') birdc 217 | ``` 218 | ``` 219 | bird> show protocols all neighbor_v4_1 220 | name proto table state since info 221 | neighbor_v4_1 BGP master up 16:10:27 Established 222 | Preference: 100 223 | Input filter: ACCEPT 224 | Output filter: packet_bgp 225 | Routes: 0 imported, 1 exported, 0 preferred 226 | Route change stats: received rejected filtered ignored accepted 227 | Import updates: 0 0 0 0 0 228 | Import withdraws: 0 0 --- 0 0 229 | Export updates: 1 0 0 --- 1 230 | Export withdraws: 0 --- --- --- 0 231 | BGP state: Established 232 | Neighbor address: 10.99.182.128 233 | Neighbor AS: 65530 234 | Neighbor ID: 147.75.36.73 235 | Neighbor caps: refresh restart-aware llgr-aware AS4 236 | Session: external AS4 237 | Source address: 10.99.182.129 238 | Hold timer: 59/90 239 | Keepalive timer: 20/30 240 | ``` 241 | 242 | **Note:** If you have bpg enabled over both ipv4 and ipv6 on your server, there will be separate running instances of the bird daemon for each protocol. Thus to verify an ipv6 peering session, in the above command you would use `birdc6` instead of `birdc`. 243 | 244 | In this case we only have a single elastic IP bound to interface lo, and we see the prefix is being exported and accepted so we are done. 245 | -------------------------------------------------------------------------------- /routers/frr/README.md: -------------------------------------------------------------------------------- 1 | ## FRR 2 | 3 | Similar to Bird, FRR is a routing daemon for Linux. It can be used to announce an elastic IP address via BGP from the instance that is currently using it. 4 | 5 | ### Requirements 6 | 7 | * A BGP enabled project and instance 8 | * An elastic IP configured on interface lo 9 | 10 | The former can be accomplished via the Packet client portal while the latter can be done via the following two commands (this applies to Ubuntu; network configuration will typically vary by distro): 11 | 12 | ```bash 13 | echo 'auto lo:0 14 | iface lo:0 inet static 15 | address 10.99.182.254 16 | netmask 255.255.255.255' >> /etc/network/interfaces 17 | ``` 18 | 19 | In the above command, you will replace `10.99.182.254` with your own elastic IP. Then simply bring up interface `lo:0`: 20 | 21 | ```bash 22 | ifup lo:0 23 | ``` 24 | 25 | ### Configuration 26 | 27 | #### Method 1: FRR on Baremetal 28 | 29 | First install system dependencies. On Ubuntu 18.04 this looks like: 30 | 31 | ```bash 32 | apt -y update && apt -y install python3.6 python3-pip git libpcre3-dev apt-transport-https ca-certificates curl wget logrotate libc-ares2 libjson-c3 vim systemd procps libreadline7 gnupg2 lsb-release apt-utils 33 | ``` 34 | 35 | Add FRR's apt signing key and configure the FRR repo: 36 | 37 | ```bash 38 | curl -s https://deb.frrouting.org/frr/keys.asc | apt-key add - && \ 39 | echo deb https://deb.frrouting.org/frr $(lsb_release -s -c) frr-stable | tee -a /etc/apt/sources.list.d/frr.list 40 | ``` 41 | 42 | Now install FRR: 43 | 44 | ```bash 45 | apt -y update && apt -y install frr frr-pythontools 46 | ``` 47 | 48 | Once installed, we need to enable BGP wiithin FRR's configuration: 49 | 50 | ```bash 51 | sed -i "s/^bgpd=no/bgpd=yes/" /etc/frr/daemons 52 | ``` 53 | 54 | Now we need to clone the network-helpers repo and install python dependencies: 55 | 56 | ```bash 57 | cd /opt 58 | git clone https://github.com/packethost/network-helpers.git 59 | cd network-helpers 60 | pip3 install jmespath 61 | pip3 install -e . 62 | ``` 63 | 64 | And then apply the FRR configuration using our instance's metadata: 65 | 66 | ``` 67 | ./configure.py -r frr | tee /etc/frr/frr.conf 68 | frr defaults traditional 69 | log syslog informational 70 | ipv6 forwarding 71 | service integrated-vtysh-config 72 | ! 73 | ! 74 | router bgp 65000 75 | bgp ebgp-requires-policy 76 | neighbor V4 peer-group 77 | neighbor V4 remote-as 65530 78 | neighbor V4 password somepassword 79 | neighbor 10.99.182.128 peer-group V4 80 | neighbor V6 peer-group 81 | neighbor V6 remote-as 65530 82 | neighbor V6 password somepassword 83 | neighbor 2604:1380:1:5f00:: peer-group V6 84 | ! 85 | address-family ipv4 unicast 86 | redistribute connected 87 | neighbor V4 route-map IMPORT in 88 | neighbor V4 route-map EXPORT out 89 | exit-address-family 90 | ! 91 | address-family ipv6 unicast 92 | redistribute connected 93 | neighbor V6 activate 94 | neighbor V6 route-map IMPORT in 95 | neighbor V6 route-map EXPORT out 96 | exit-address-family 97 | ! 98 | route-map EXPORT deny 100 99 | ! 100 | route-map EXPORT permit 1 101 | match interface lo 102 | ! 103 | route-map IMPORT deny 1 104 | ! 105 | line vty 106 | ! 107 | ``` 108 | 109 | Lastly, we'll restart FRR: 110 | 111 | ```bash 112 | systemctl restart frr 113 | ``` 114 | 115 | And then verify our bgp session is up and our desired prefix is being advertised: 116 | 117 | ``` 118 | vtysh 119 | 120 | Hello, this is FRRouting (version 7.3). 121 | Copyright 1996-2005 Kunihiro Ishiguro, et al. 122 | # 123 | ``` 124 | ``` 125 | # show bgp summary 126 | 127 | IPv4 Unicast Summary: 128 | BGP router identifier 10.99.182.254, local AS number 65000 vrf-id 0 129 | BGP table version 3 130 | RIB entries 5, using 920 bytes of memory 131 | Peers 2, using 41 KiB of memory 132 | Peer groups 2, using 128 bytes of memory 133 | 134 | Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd 135 | 10.99.182.128 4 65530 6 7 0 0 0 00:01:17 0 136 | 2604:1380:1:5f00:: 4 65530 7 9 0 0 0 00:01:17 NoNeg 137 | 138 | Total number of neighbors 2 139 | 140 | IPv6 Unicast Summary: 141 | BGP router identifier 10.99.182.254, local AS number 65000 vrf-id 0 142 | BGP table version 1 143 | RIB entries 1, using 184 bytes of memory 144 | Peers 1, using 20 KiB of memory 145 | Peer groups 2, using 128 bytes of memory 146 | 147 | Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd 148 | 2604:1380:1:5f00:: 4 65530 7 9 0 0 0 00:01:17 0 149 | 150 | Total number of neighbors 1 151 | ``` 152 | ``` 153 | # show ip bgp neighbors 10.99.182.128 advertised-routes 154 | BGP table version is 3, local router ID is 10.99.182.254, vrf id 0 155 | Default local pref 100, local AS 65000 156 | Status codes: s suppressed, d damped, h history, * valid, > best, = multipath, 157 | i internal, r RIB-failure, S Stale, R Removed 158 | Nexthop codes: @NNN nexthop's vrf id, < announce-nh-self 159 | Origin codes: i - IGP, e - EGP, ? - incomplete 160 | 161 | Network Next Hop Metric LocPrf Weight Path 162 | *> 10.99.182.254/32 0.0.0.0 0 32768 ? 163 | 164 | Total number of prefixes 1 165 | ``` 166 | 167 | #### Method 2: FRR via Docker 168 | 169 | Using your OS's package management utility, install docker, docker-compose and git if not already installed. On Ubuntu 18.04 this looks like: 170 | 171 | ```bash 172 | apt -y update && apt -y install docker docker-compose git 173 | systemctl enable docker && systemctl start docker 174 | ``` 175 | 176 | Clone the repo: 177 | 178 | ```bash 179 | cd /opt 180 | git clone https://github.com/packethost/network-helpers.git 181 | ``` 182 | 183 | Build the image: 184 | 185 | ```bash 186 | cd network-helpers 187 | docker build -f routers/frr/Dockerfile -t local/frr:latest . 188 | ``` 189 | 190 | Up the container: 191 | 192 | ```bash 193 | cd routers/frr 194 | docker-compose up -d 195 | ``` 196 | 197 | To verify that FRR is configured and up, we can review the container logs: 198 | 199 | ``` 200 | docker logs $(docker ps | awk '$2 == "local/frr:latest" {print $1}') 201 | + /opt/bgp/configure.py -r frr 202 | + tee /etc/frr/frr.conf 203 | frr defaults traditional 204 | log syslog informational 205 | ipv6 forwarding 206 | service integrated-vtysh-config 207 | ! 208 | ! 209 | router bgp 65000 210 | bgp ebgp-requires-policy 211 | neighbor V4 peer-group 212 | neighbor V4 remote-as 65530 213 | neighbor V4 password somepassword 214 | neighbor 10.99.182.128 peer-group V4 215 | neighbor V6 peer-group 216 | neighbor V6 remote-as 65530 217 | neighbor V6 password somepassword 218 | neighbor 2604:1380:1:5f00:: peer-group V6 219 | ! 220 | address-family ipv4 unicast 221 | redistribute connected 222 | neighbor V4 route-map IMPORT in 223 | neighbor V4 route-map EXPORT out 224 | exit-address-family 225 | ! 226 | address-family ipv6 unicast 227 | redistribute connected 228 | neighbor V6 activate 229 | neighbor V6 route-map IMPORT in 230 | neighbor V6 route-map EXPORT out 231 | exit-address-family 232 | ! 233 | route-map EXPORT deny 100 234 | ! 235 | route-map EXPORT permit 1 236 | match interface lo 237 | ! 238 | route-map IMPORT deny 1 239 | ! 240 | line vty 241 | ! 242 | + '[' 0 '!=' 0 ']' 243 | + /etc/init.d/frr start 244 | Started watchfrr. 245 | + exec sleep 10000d 246 | ``` 247 | 248 | Lastly, we need to verify that our bgp sessions are established, and the desired prefixes are being exported. FRR has a Cisco-like cli (vtysh) that we can use: 249 | 250 | ```bash 251 | docker exec -it $(docker ps | awk '$2 == "local/frr:latest" {print $1}') vtysh 252 | frr# 253 | ``` 254 | 255 | Then to check out sessions: 256 | 257 | ``` 258 | frr# show bgp summary 259 | 260 | IPv4 Unicast Summary: 261 | BGP router identifier 172.17.0.1, local AS number 65000 vrf-id 0 262 | BGP table version 3 263 | RIB entries 5, using 920 bytes of memory 264 | Peers 2, using 41 KiB of memory 265 | Peer groups 2, using 128 bytes of memory 266 | 267 | Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd 268 | 10.99.182.128 4 65530 37 35 0 0 0 00:15:23 0 269 | 2604:1380:1:5f00:: 4 65530 37 34 0 0 0 00:15:23 NoNeg 270 | 271 | Total number of neighbors 2 272 | 273 | IPv6 Unicast Summary: 274 | BGP router identifier 172.17.0.1, local AS number 65000 vrf-id 0 275 | BGP table version 1 276 | RIB entries 1, using 184 bytes of memory 277 | Peers 1, using 20 KiB of memory 278 | Peer groups 2, using 128 bytes of memory 279 | 280 | Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd 281 | 2604:1380:1:5f00:: 4 65530 37 34 0 0 0 00:15:23 0 282 | 283 | Total number of neighbors 1 284 | ``` 285 | 286 | And finally to verify that our prefix bound to interface lo is being exported: 287 | 288 | ``` 289 | frr# show ip bgp neighbors 10.99.182.128 advertised-routes 290 | BGP table version is 3, local router ID is 172.17.0.1, vrf id 0 291 | Default local pref 100, local AS 65000 292 | Status codes: s suppressed, d damped, h history, * valid, > best, = multipath, 293 | i internal, r RIB-failure, S Stale, R Removed 294 | Nexthop codes: @NNN nexthop's vrf id, < announce-nh-self 295 | Origin codes: i - IGP, e - EGP, ? - incomplete 296 | 297 | Network Next Hop Metric LocPrf Weight Path 298 | *> 10.99.182.254/32 0.0.0.0 0 32768 ? 299 | 300 | Total number of prefixes 1 301 | ``` 302 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------