├── .github └── workflows │ ├── build.yml │ ├── integration_test.yml │ ├── static_analysis.yml │ └── unit_test.yml ├── .gitignore ├── LICENSE ├── README.md ├── example_update_decoder_test.go ├── examples └── features │ └── main.go ├── fsm.go ├── go.mod ├── go.sum ├── iana_const.go ├── iana_gen.go ├── logger.go ├── notification_error.go ├── packet.go ├── packet_test.go ├── peer.go ├── peer_options.go ├── plugin.go ├── server.go ├── server_test.go ├── tcp_md5_sig.go ├── tcp_md5_sig_linux.go ├── tcp_md5_sig_linux_test.go ├── test ├── Dockerfile ├── bird_test.go └── docker-compose.yml ├── testdata └── fuzz │ └── FuzzUpdateDecoder_Decode │ ├── 0ea655d260c00382 │ └── c5c288406e9f4d1c ├── update.go └── update_test.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | go-version: [1.21.x] 13 | os: [ubuntu-latest] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - name: Install Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | - name: Checkout code 21 | uses: actions/checkout@v2 22 | - name: Build 23 | run: go build examples/features/main.go 24 | -------------------------------------------------------------------------------- /.github/workflows/integration_test.yml: -------------------------------------------------------------------------------- 1 | name: integration_test 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | jobs: 9 | integration_test: 10 | defaults: 11 | run: 12 | working-directory: test 13 | strategy: 14 | matrix: 15 | go-version: [1.21.x] 16 | os: [ubuntu-latest] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v2 21 | - name: start bird container 22 | run: docker-compose up -d bird 23 | - name: bird integration test 24 | run: docker-compose up --abort-on-container-exit --exit-code-from corebgp 25 | -------------------------------------------------------------------------------- /.github/workflows/static_analysis.yml: -------------------------------------------------------------------------------- 1 | name: static_analysis 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | jobs: 9 | static_analysis: 10 | name: static_analysis 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out code 14 | uses: actions/checkout@v3 15 | - name: Set up Go 16 | uses: actions/setup-go@v3 17 | with: 18 | go-version-file: go.mod 19 | - name: Install goimports 20 | run: "go install golang.org/x/tools/cmd/goimports@latest" 21 | - name: Run gofmt (goimports) 22 | run: | 23 | OUT="$(goimports --format-only -d .)" 24 | if [ -n "$OUT" ]; then echo "${OUT}"; fi 25 | - name: Run go vet 26 | run: "go vet ./..." 27 | - name: Run staticcheck 28 | uses: dominikh/staticcheck-action@v1.3.0 29 | with: 30 | version: "2023.1.6" 31 | install-go: false -------------------------------------------------------------------------------- /.github/workflows/unit_test.yml: -------------------------------------------------------------------------------- 1 | name: unit_test 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | jobs: 9 | unit_test: 10 | strategy: 11 | matrix: 12 | go-version: [1.21.x] 13 | os: [ubuntu-latest] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - name: Install Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | - name: Checkout code 21 | uses: actions/checkout@v2 22 | - name: Test 23 | run: go test -v -race ./... 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jordan Whited 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoreBGP 2 | 3 | [![GoDev](https://img.shields.io/static/v1?label=godev&message=reference&color=00add8)](https://pkg.go.dev/github.com/jwhited/corebgp) 4 | 5 | CoreBGP is a BGP library written in Go that implements the BGP FSM with an event-driven, pluggable model. It exposes an API that empowers the user to: 6 | * send and validate OPEN message capabilities 7 | * handle "important" state transitions 8 | * handle incoming UPDATE messages 9 | * send outgoing UPDATE messages 10 | 11 | CoreBGP provides optional, composable UPDATE message decoding facilities via [UpdateDecoder](https://pkg.go.dev/github.com/jwhited/corebgp#example-UpdateDecoder). CoreBGP does not manage a routing table or send its own UPDATE messages. Those responsibilities are passed down to the user. Therefore, the intended user is someone who wants that responsibility. 12 | 13 | See this [blog post](https://www.jordanwhited.com/posts/corebgp-plugging-in-to-bgp/) for the background and reasoning behind the development of CoreBGP. 14 | 15 | ## Plugin 16 | The primary building block of CoreBGP is a Plugin, defined by the following interface: 17 | ```go 18 | // Plugin is a BGP peer plugin. 19 | type Plugin interface { 20 | // GetCapabilities is fired when a peer's FSM is in the Connect state prior 21 | // to sending an Open message. The returned capabilities are included in the 22 | // Open message sent to the peer. 23 | // 24 | // The four-octet AS number space capability will be implicitly handled, 25 | // Plugin implementations are not required to return it. 26 | GetCapabilities(peer PeerConfig) []Capability 27 | 28 | // OnOpenMessage is fired when an Open message is received from a peer 29 | // during the OpenSent state. Returning a non-nil Notification will cause it 30 | // to be sent to the peer and the FSM will transition to the Idle state. 31 | // 32 | // Remote peers MUST include the four-octet AS number space capability in 33 | // their open message. corebgp will return a Notification message if a 34 | // remote peer does not support said capability, and will not invoke 35 | // OnOpenMessage. 36 | // 37 | // Per RFC5492 a BGP speaker should only send a Notification if a required 38 | // capability is missing; unknown or unsupported capabilities should be 39 | // ignored. 40 | OnOpenMessage(peer PeerConfig, routerID netip.Addr, capabilities []Capability) *Notification 41 | 42 | // OnEstablished is fired when a peer's FSM transitions to the Established 43 | // state. The returned UpdateMessageHandler will be fired when an Update 44 | // message is received from the peer. 45 | // 46 | // The provided writer can be used to send Update messages to the peer for 47 | // the lifetime of the FSM's current, established state. It should be 48 | // discarded once OnClose() fires. 49 | OnEstablished(peer PeerConfig, writer UpdateMessageWriter) UpdateMessageHandler 50 | 51 | // OnClose is fired when a peer's FSM transitions out of the Established 52 | // state. 53 | OnClose(peer PeerConfig) 54 | } 55 | ``` 56 | 57 | Here's an example Plugin that logs when a peer enters/leaves an established state and when an UPDATE message is received: 58 | ```go 59 | type plugin struct{} 60 | 61 | func (p *plugin) GetCapabilities(c corebgp.PeerConfig) []corebgp.Capability { 62 | caps := make([]corebgp.Capability, 0) 63 | return caps 64 | } 65 | 66 | func (p *plugin) OnOpenMessage(peer corebgp.PeerConfig, routerID netip.Addr, capabilities []corebgp.Capability) *corebgp.Notification { 67 | return nil 68 | } 69 | 70 | func (p *plugin) OnEstablished(peer corebgp.PeerConfig, writer corebgp.UpdateMessageWriter) corebgp.UpdateMessageHandler { 71 | log.Println("peer established") 72 | // send End-of-Rib 73 | writer.WriteUpdate([]byte{0, 0, 0, 0}) 74 | return p.handleUpdate 75 | } 76 | 77 | func (p *plugin) OnClose(peer corebgp.PeerConfig) { 78 | log.Println("peer closed") 79 | } 80 | 81 | func (p *plugin) handleUpdate(peer corebgp.PeerConfig, u []byte) *corebgp.Notification { 82 | log.Printf("got update message of len: %d", len(u)) 83 | return nil 84 | } 85 | ``` 86 | 87 | Plugins are attached to peers when they are added to the Server, which manages their lifetime: 88 | ``` go 89 | routerID := netip.MustParseAddr("192.0.2.1") 90 | srv, err := corebgp.NewServer(routerID) 91 | if err != nil { 92 | log.Fatalf("error constructing server: %v", err) 93 | } 94 | p := &plugin{} 95 | err = srv.AddPeer(corebgp.PeerConfig{ 96 | RemoteAddress: netip.MustParseAddr("198.51.100.10"), 97 | LocalAS: 65001, 98 | RemoteAS: 65010, 99 | }, p, corebgp.WithLocalAddress(routerID)) 100 | if err != nil { 101 | log.Fatalf("error adding peer: %v", err) 102 | } 103 | ``` 104 | 105 | For more examples check out the [examples directory](https://github.com/jwhited/corebgp/tree/main/examples) and [pkg.go.dev](https://pkg.go.dev/github.com/jwhited/corebgp?tab=doc) for the complete API. 106 | 107 | ## Versioning 108 | CoreBGP follows [semver](https://semver.org) as closely as it can. Seeing as we are still major version zero (0.y.z), the public API should not be considered stable. You are encouraged to pin CoreBGP's version with your dependency management solution of choice. 109 | -------------------------------------------------------------------------------- /example_update_decoder_test.go: -------------------------------------------------------------------------------- 1 | package corebgp_test 2 | 3 | import ( 4 | "fmt" 5 | "net/netip" 6 | 7 | "github.com/jwhited/corebgp" 8 | ) 9 | 10 | type updateMessage struct { 11 | withdrawn []netip.Prefix 12 | origin uint8 13 | asPath []uint32 14 | nextHop netip.Addr 15 | communities []uint32 16 | nlri []netip.Prefix 17 | ipv6NextHops []netip.Addr 18 | ipv6NLRI []netip.Prefix 19 | ipv6Withdrawn []netip.Prefix 20 | } 21 | 22 | func newPathAttrsDecodeFn() func(m *updateMessage, code uint8, flags corebgp.PathAttrFlags, b []byte) error { 23 | reachDecodeFn := corebgp.NewMPReachNLRIDecodeFn[*updateMessage]( 24 | func(m *updateMessage, afi uint16, safi uint8, nh, nlri []byte) error { 25 | if afi == corebgp.AFI_IPV6 && safi == corebgp.SAFI_UNICAST { 26 | nhs, err := corebgp.DecodeMPReachIPv6NextHops(nh) 27 | if err != nil { 28 | return err 29 | } 30 | prefixes, err := corebgp.DecodeMPIPv6Prefixes(nlri) 31 | if err != nil { 32 | return err 33 | } 34 | m.ipv6NextHops = nhs 35 | m.ipv6NLRI = prefixes 36 | } 37 | return nil 38 | }, 39 | ) 40 | unreachDecodeFn := corebgp.NewMPUnreachNLRIDecodeFn[*updateMessage]( 41 | func(m *updateMessage, afi uint16, safi uint8, withdrawn []byte) error { 42 | if afi == corebgp.AFI_IPV6 && safi == corebgp.SAFI_UNICAST { 43 | prefixes, err := corebgp.DecodeMPIPv6Prefixes(withdrawn) 44 | if err != nil { 45 | return err 46 | } 47 | m.ipv6Withdrawn = prefixes 48 | } 49 | return nil 50 | }, 51 | ) 52 | return func(m *updateMessage, code uint8, flags corebgp.PathAttrFlags, b []byte) error { 53 | switch code { 54 | case corebgp.PATH_ATTR_ORIGIN: 55 | var o corebgp.OriginPathAttr 56 | err := o.Decode(flags, b) 57 | if err != nil { 58 | return err 59 | } 60 | m.origin = uint8(o) 61 | return nil 62 | case corebgp.PATH_ATTR_AS_PATH: 63 | var a corebgp.ASPathAttr 64 | err := a.Decode(flags, b) 65 | if err != nil { 66 | return err 67 | } 68 | m.asPath = a.ASSequence 69 | return nil 70 | case corebgp.PATH_ATTR_NEXT_HOP: 71 | var nh corebgp.NextHopPathAttr 72 | err := nh.Decode(flags, b) 73 | if err != nil { 74 | return err 75 | } 76 | m.nextHop = netip.Addr(nh) 77 | return nil 78 | case corebgp.PATH_ATTR_COMMUNITY: 79 | var comms corebgp.CommunitiesPathAttr 80 | err := comms.Decode(flags, b) 81 | if err != nil { 82 | return err 83 | } 84 | m.communities = comms 85 | case corebgp.PATH_ATTR_MP_REACH_NLRI: 86 | return reachDecodeFn(m, flags, b) 87 | case corebgp.PATH_ATTR_MP_UNREACH_NLRI: 88 | return unreachDecodeFn(m, flags, b) 89 | } 90 | return nil 91 | } 92 | } 93 | 94 | // ExampleUpdateDecoder demonstrates an UpdateDecoder that decodes UPDATE 95 | // messages containing IPv4 and IPv6 routes. 96 | func ExampleUpdateDecoder() { 97 | ud := corebgp.NewUpdateDecoder[*updateMessage]( 98 | corebgp.NewWithdrawnRoutesDecodeFn[*updateMessage](func(u *updateMessage, withdrawn []netip.Prefix) error { 99 | u.withdrawn = withdrawn 100 | return nil 101 | }), 102 | newPathAttrsDecodeFn(), 103 | corebgp.NewNLRIDecodeFn[*updateMessage](func(u *updateMessage, nlri []netip.Prefix) error { 104 | u.nlri = nlri 105 | return nil 106 | }), 107 | ) 108 | 109 | m := &updateMessage{} 110 | fmt.Println("=== ipv4 ===") 111 | fmt.Println(ud.Decode(m, []byte{ 112 | 0x00, 0x03, // withdrawn routes length 113 | 0x10, 0x0a, 0x00, // withdrawn 10.0.0.0/16 114 | 0x00, 0x1b, // total path attr len 115 | 0x40, 0x01, 0x01, 0x01, // origin egp 116 | 0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xea, // as path 65002 117 | 0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x02, // next hop 192.0.2.2 118 | 0xc0, 0x08, 0x04, 0xfd, 0xea, 0xff, 0xff, // communities 65002:65535 119 | 0x18, 0xc0, 0x00, 0x02, // nlri 192.0.2.0/24 120 | })) 121 | fmt.Println(m.withdrawn) 122 | fmt.Println(m.origin) 123 | fmt.Println(m.asPath) 124 | fmt.Println(m.nextHop) 125 | fmt.Println(m.nlri) 126 | fmt.Println(m.communities) 127 | 128 | m = &updateMessage{} 129 | fmt.Println("=== ipv6 ===") 130 | fmt.Println(ud.Decode(m, []byte{ 131 | 0x00, 0x00, // withdrawn routes length 132 | 0x00, 0x3f, // total path attr len 133 | // extended len MP_REACH_NLRI 2001:db8::/64 nhs 2001:db8::2 & fe80::42:c0ff:fe00:202 134 | 0x90, 0x0e, 0x00, 0x2e, 0x00, 0x02, 0x01, 0x20, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0xc0, 0xff, 0xfe, 0x00, 0x02, 0x02, 0x00, 0x40, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 135 | 0x40, 0x01, 0x01, 0x01, // origin egp 136 | 0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xea, // as path 65002 137 | })) 138 | fmt.Println(m.origin) 139 | fmt.Println(m.asPath) 140 | fmt.Println(m.ipv6NextHops) 141 | fmt.Println(m.ipv6NLRI) 142 | 143 | // Output: 144 | // === ipv4 === 145 | // 146 | // [10.0.0.0/16] 147 | // 1 148 | // [65002] 149 | // 192.0.2.2 150 | // [192.0.2.0/24] 151 | // [4260036607] 152 | // === ipv6 === 153 | // 154 | // 1 155 | // [65002] 156 | // [2001:db8::2 fe80::42:c0ff:fe00:202] 157 | // [2001:db8::/64] 158 | } 159 | -------------------------------------------------------------------------------- /examples/features/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net" 9 | "net/netip" 10 | "os" 11 | "os/signal" 12 | "strings" 13 | "syscall" 14 | 15 | "github.com/jwhited/corebgp" 16 | ) 17 | 18 | var ( 19 | routerID = flag.String("id", "", "router ID") 20 | localAS = flag.Uint("las", 0, "local AS") 21 | remoteAS = flag.Uint("ras", 0, "remote AS") 22 | localAddress = flag.String("laddr", "", "local address") 23 | remoteAddress = flag.String("raddr", "", "remote address") 24 | ipv4 = flag.Bool("v4", false, "enable ipv4 afi/safi") 25 | ipv6 = flag.Bool("v6", false, "enable ipv6 afi/safi") 26 | bindAddr = flag.String("bind", ":179", "listen address") 27 | passive = flag.Bool("passive", false, "disable outbound connections") 28 | md5 = flag.String("md5", "", "tcp md5 signature") 29 | addPath = flag.Bool("add-path", false, "enable add-path") 30 | ) 31 | 32 | type updateMessage struct { 33 | addPathIPv4 bool 34 | addPathIPv6 bool 35 | withdrawn []netip.Prefix 36 | addPathWithdrawn []corebgp.AddPathPrefix 37 | origin uint8 38 | asPath []uint32 39 | nextHop netip.Addr 40 | communities []uint32 41 | largeCommunities corebgp.LargeCommunitiesPathAttr 42 | localPref uint32 43 | med uint32 44 | nlri []netip.Prefix 45 | addPathNLRI []corebgp.AddPathPrefix 46 | ipv6NextHops []netip.Addr 47 | ipv6NLRI []netip.Prefix 48 | addPathIPv6NLRI []corebgp.AddPathPrefix 49 | ipv6Withdrawn []netip.Prefix 50 | addPathIPv6Withdrawn []corebgp.AddPathPrefix 51 | } 52 | 53 | func fmtSlice[T any](t []T, name string, sb *strings.Builder) { 54 | if len(t) > 0 { 55 | if sb.Len() > 0 { 56 | sb.WriteString(" ") 57 | } 58 | sb.WriteString(fmt.Sprintf("%s=%v", name, t)) 59 | } 60 | } 61 | 62 | func (u updateMessage) String() string { 63 | commsFmt := func(in []uint32) []string { 64 | comms := make([]string, 0, len(in)) 65 | for _, c := range in { 66 | comms = append(comms, fmt.Sprintf("%d:%d", c>>16, c&0x0000FFFF)) 67 | } 68 | return comms 69 | } 70 | largeCommsFmt := func(in corebgp.LargeCommunitiesPathAttr) []string { 71 | lc := make([]string, 0, len(in)) 72 | for _, c := range in { 73 | lc = append(lc, fmt.Sprintf("%d:%d:%d", c.GlobalAdmin, c.LocalData1, c.LocalData2)) 74 | } 75 | return lc 76 | } 77 | var sb strings.Builder 78 | fmtSlice[netip.Prefix](u.nlri, "nlri", &sb) 79 | fmtSlice[netip.Prefix](u.ipv6NLRI, "ipv6NLRI", &sb) 80 | fmtSlice[corebgp.AddPathPrefix](u.addPathNLRI, "addPathNLRI", &sb) 81 | fmtSlice[corebgp.AddPathPrefix](u.addPathIPv6NLRI, "addPathIPv6NLRI", &sb) 82 | if len(u.nlri) > 0 || len(u.ipv6NLRI) > 0 || len(u.addPathNLRI) > 0 || len(u.addPathIPv6NLRI) > 0 { 83 | sb.WriteString(fmt.Sprintf(" origin=%v", u.origin)) 84 | if len(u.nlri) > 0 { 85 | sb.WriteString(fmt.Sprintf(" nextHop=%v", u.nextHop)) 86 | } 87 | if len(u.ipv6NLRI) > 0 { 88 | fmtSlice[netip.Addr](u.ipv6NextHops, "ipv6NextHops", &sb) 89 | } 90 | } 91 | if u.med > 0 { 92 | sb.WriteString(fmt.Sprintf(" med=%d", u.med)) 93 | } 94 | if u.localPref > 0 { 95 | sb.WriteString(fmt.Sprintf(" localPref=%d", u.localPref)) 96 | } 97 | fmtSlice[uint32](u.asPath, "asPath", &sb) 98 | fmtSlice[string](commsFmt(u.communities), "communities", &sb) 99 | fmtSlice[string](largeCommsFmt(u.largeCommunities), "large-communities", &sb) 100 | fmtSlice[netip.Prefix](u.withdrawn, "withdrawn", &sb) 101 | fmtSlice[netip.Prefix](u.ipv6Withdrawn, "ipv6Withdrawn", &sb) 102 | fmtSlice[corebgp.AddPathPrefix](u.addPathWithdrawn, "addPathWithdrawn", &sb) 103 | fmtSlice[corebgp.AddPathPrefix](u.addPathIPv6Withdrawn, "addPathIPv6Withdrawn", &sb) 104 | if sb.Len() == 0 { 105 | return "End-of-RIB" 106 | } 107 | return sb.String() 108 | } 109 | 110 | func newPathAttrsDecodeFn() func(m *updateMessage, code uint8, flags corebgp.PathAttrFlags, b []byte) error { 111 | reachDecodeFn := corebgp.NewMPReachNLRIDecodeFn[*updateMessage]( 112 | func(m *updateMessage, afi uint16, safi uint8, nh, nlri []byte) error { 113 | if afi == corebgp.AFI_IPV6 && safi == corebgp.SAFI_UNICAST { 114 | nhs, err := corebgp.DecodeMPReachIPv6NextHops(nh) 115 | if err != nil { 116 | return err 117 | } 118 | if m.addPathIPv6 { 119 | prefixes, err := corebgp.DecodeMPIPv6AddPathPrefixes(nlri) 120 | if err != nil { 121 | return err 122 | } 123 | m.addPathIPv6NLRI = prefixes 124 | } else { 125 | prefixes, err := corebgp.DecodeMPIPv6Prefixes(nlri) 126 | if err != nil { 127 | return err 128 | } 129 | m.ipv6NLRI = prefixes 130 | } 131 | 132 | m.ipv6NextHops = nhs 133 | } 134 | return nil 135 | }, 136 | ) 137 | unreachDecodeFn := corebgp.NewMPUnreachNLRIDecodeFn[*updateMessage]( 138 | func(m *updateMessage, afi uint16, safi uint8, withdrawn []byte) error { 139 | if afi == corebgp.AFI_IPV6 && safi == corebgp.SAFI_UNICAST { 140 | if m.addPathIPv6 { 141 | prefixes, err := corebgp.DecodeMPIPv6AddPathPrefixes(withdrawn) 142 | if err != nil { 143 | return err 144 | } 145 | m.addPathIPv6Withdrawn = prefixes 146 | } else { 147 | prefixes, err := corebgp.DecodeMPIPv6Prefixes(withdrawn) 148 | if err != nil { 149 | return err 150 | } 151 | m.ipv6Withdrawn = prefixes 152 | } 153 | } 154 | return nil 155 | }, 156 | ) 157 | return func(m *updateMessage, code uint8, flags corebgp.PathAttrFlags, b []byte) error { 158 | switch code { 159 | case corebgp.PATH_ATTR_ORIGIN: 160 | var o corebgp.OriginPathAttr 161 | err := o.Decode(flags, b) 162 | if err != nil { 163 | return err 164 | } 165 | m.origin = uint8(o) 166 | return nil 167 | case corebgp.PATH_ATTR_AS_PATH: 168 | var a corebgp.ASPathAttr 169 | err := a.Decode(flags, b) 170 | if err != nil { 171 | return err 172 | } 173 | m.asPath = a.ASSequence 174 | return nil 175 | case corebgp.PATH_ATTR_NEXT_HOP: 176 | var nh corebgp.NextHopPathAttr 177 | err := nh.Decode(flags, b) 178 | if err != nil { 179 | return err 180 | } 181 | m.nextHop = netip.Addr(nh) 182 | return nil 183 | case corebgp.PATH_ATTR_COMMUNITY: 184 | var comms corebgp.CommunitiesPathAttr 185 | err := comms.Decode(flags, b) 186 | if err != nil { 187 | return err 188 | } 189 | m.communities = comms 190 | case corebgp.PATH_ATTR_LOCAL_PREF: 191 | var lpref corebgp.LocalPrefPathAttr 192 | if err := lpref.Decode(flags, b); err != nil { 193 | return err 194 | } 195 | m.localPref = uint32(lpref) 196 | case corebgp.PATH_ATTR_LARGE_COMMUNITY: 197 | var lc corebgp.LargeCommunitiesPathAttr 198 | if err := lc.Decode(flags, b); err != nil { 199 | return err 200 | } 201 | m.largeCommunities = lc 202 | case corebgp.PATH_ATTR_MED: 203 | var med corebgp.MEDPathAttr 204 | if err := med.Decode(flags, b); err != nil { 205 | return err 206 | } 207 | m.med = uint32(med) 208 | case corebgp.PATH_ATTR_MP_REACH_NLRI: 209 | return reachDecodeFn(m, flags, b) 210 | case corebgp.PATH_ATTR_MP_UNREACH_NLRI: 211 | return unreachDecodeFn(m, flags, b) 212 | } 213 | return nil 214 | } 215 | } 216 | 217 | type plugin struct { 218 | ud *corebgp.UpdateDecoder[*updateMessage] 219 | addPathIPv4, addPathIPv6 bool 220 | } 221 | 222 | func (p *plugin) GetCapabilities(c corebgp.PeerConfig) []corebgp.Capability { 223 | caps := make([]corebgp.Capability, 0) 224 | if *ipv4 { 225 | caps = append(caps, corebgp.NewMPExtensionsCapability(corebgp.AFI_IPV4, corebgp.SAFI_UNICAST)) 226 | } 227 | if *ipv6 { 228 | caps = append(caps, corebgp.NewMPExtensionsCapability(corebgp.AFI_IPV6, corebgp.SAFI_UNICAST)) 229 | } 230 | if *addPath { 231 | tuples := make([]corebgp.AddPathTuple, 0) 232 | tuples = append(tuples, corebgp.AddPathTuple{ 233 | AFI: corebgp.AFI_IPV4, 234 | SAFI: corebgp.SAFI_UNICAST, 235 | Tx: true, 236 | Rx: true, 237 | }) 238 | if *ipv6 { 239 | tuples = append(tuples, corebgp.AddPathTuple{ 240 | AFI: corebgp.AFI_IPV6, 241 | SAFI: corebgp.SAFI_UNICAST, 242 | Tx: true, 243 | Rx: true, 244 | }) 245 | } 246 | caps = append(caps, corebgp.NewAddPathCapability(tuples)) 247 | } 248 | return caps 249 | } 250 | 251 | func (p *plugin) OnOpenMessage(peer corebgp.PeerConfig, routerID netip.Addr, capabilities []corebgp.Capability) *corebgp.Notification { 252 | log.Println("open message received") 253 | if *addPath { 254 | p.addPathIPv4 = false 255 | p.addPathIPv6 = false 256 | for _, c := range capabilities { 257 | if c.Code != corebgp.CAP_ADD_PATH { 258 | continue 259 | } 260 | tuples, err := corebgp.DecodeAddPathTuples(c.Value) 261 | if err != nil { 262 | return err.(*corebgp.Notification) 263 | } 264 | for _, tuple := range tuples { 265 | if tuple.SAFI != corebgp.SAFI_UNICAST || !tuple.Tx { 266 | continue 267 | } 268 | if tuple.AFI == corebgp.AFI_IPV4 { 269 | p.addPathIPv4 = true 270 | } else if tuple.AFI == corebgp.AFI_IPV6 { 271 | p.addPathIPv6 = true 272 | } 273 | } 274 | } 275 | } 276 | return nil 277 | } 278 | 279 | func (p *plugin) OnEstablished(peer corebgp.PeerConfig, writer corebgp.UpdateMessageWriter) corebgp.UpdateMessageHandler { 280 | log.Println("peer established") 281 | // send End-of-Rib 282 | writer.WriteUpdate([]byte{0, 0, 0, 0}) 283 | return p.handleUpdate 284 | } 285 | 286 | func (p *plugin) OnClose(peer corebgp.PeerConfig) { 287 | log.Println("peer closed") 288 | } 289 | 290 | func (p *plugin) handleUpdate(peer corebgp.PeerConfig, b []byte) *corebgp.Notification { 291 | m := &updateMessage{ 292 | addPathIPv4: p.addPathIPv4, 293 | addPathIPv6: p.addPathIPv6, 294 | } 295 | err := p.ud.Decode(m, b) 296 | if err != nil { 297 | return corebgp.UpdateNotificationFromErr(err) 298 | } 299 | log.Printf("got update message: %s", m) 300 | return nil 301 | } 302 | 303 | func newWithdrawnRoutesDecodeFn() corebgp.DecodeFn[*updateMessage] { 304 | fn := corebgp.NewWithdrawnRoutesDecodeFn[*updateMessage](func(u *updateMessage, p []netip.Prefix) error { 305 | u.withdrawn = p 306 | return nil 307 | }) 308 | apFn := corebgp.NewWithdrawnAddPathRoutesDecodeFn[*updateMessage](func(u *updateMessage, a []corebgp.AddPathPrefix) error { 309 | u.addPathWithdrawn = a 310 | return nil 311 | }) 312 | return func(u *updateMessage, b []byte) error { 313 | if u.addPathIPv4 { 314 | return apFn(u, b) 315 | } 316 | return fn(u, b) 317 | } 318 | } 319 | 320 | func newNLRIDecodeFn() corebgp.DecodeFn[*updateMessage] { 321 | fn := corebgp.NewNLRIDecodeFn[*updateMessage](func(u *updateMessage, p []netip.Prefix) error { 322 | u.nlri = p 323 | return nil 324 | }) 325 | apFn := corebgp.NewNLRIAddPathDecodeFn[*updateMessage](func(u *updateMessage, a []corebgp.AddPathPrefix) error { 326 | u.addPathNLRI = a 327 | return nil 328 | }) 329 | return func(u *updateMessage, b []byte) error { 330 | if u.addPathIPv4 { 331 | return apFn(u, b) 332 | } 333 | return fn(u, b) 334 | } 335 | } 336 | 337 | func main() { 338 | flag.Parse() 339 | var ( 340 | lis net.Listener 341 | err error 342 | ) 343 | remote := netip.MustParseAddr(*remoteAddress) 344 | local := netip.MustParseAddr(*localAddress) 345 | if len(*bindAddr) > 0 { 346 | lc := &net.ListenConfig{} 347 | if len(*md5) > 0 { 348 | lc.Control = func(network, address string, 349 | c syscall.RawConn) error { 350 | var seterr error 351 | err := c.Control(func(fdPtr uintptr) { 352 | fd := int(fdPtr) 353 | prefixLen := uint8(32) 354 | if !remote.Is4() { 355 | prefixLen = 128 356 | } 357 | seterr = corebgp.SetTCPMD5Signature(fd, 358 | remote, prefixLen, *md5) 359 | }) 360 | if err != nil { 361 | return err 362 | } 363 | return seterr 364 | } 365 | } 366 | lis, err = lc.Listen(context.Background(), "tcp", *bindAddr) 367 | if err != nil { 368 | log.Fatalf("error constructing listener: %v", err) 369 | } 370 | } 371 | corebgp.SetLogger(log.Print) 372 | srv, err := corebgp.NewServer(netip.MustParseAddr(*routerID)) 373 | if err != nil { 374 | log.Fatalf("error constructing server: %v", err) 375 | } 376 | p := &plugin{ 377 | ud: corebgp.NewUpdateDecoder[*updateMessage]( 378 | newWithdrawnRoutesDecodeFn(), 379 | newPathAttrsDecodeFn(), 380 | newNLRIDecodeFn(), 381 | ), 382 | } 383 | peerOpts := make([]corebgp.PeerOption, 0) 384 | if len(*md5) > 0 { 385 | peerOpts = append(peerOpts, corebgp.WithDialerControl( 386 | func(network, address string, c syscall.RawConn) error { 387 | var seterr error 388 | err := c.Control(func(fdPtr uintptr) { 389 | fd := int(fdPtr) 390 | prefixLen := uint8(32) 391 | if !remote.Is4() { 392 | prefixLen = 128 393 | } 394 | seterr = corebgp.SetTCPMD5Signature(fd, 395 | remote, prefixLen, *md5) 396 | }) 397 | if err != nil { 398 | return err 399 | } 400 | return seterr 401 | })) 402 | } 403 | if *passive { 404 | peerOpts = append(peerOpts, corebgp.WithPassive()) 405 | } 406 | peerOpts = append(peerOpts, corebgp.WithLocalAddress(local)) 407 | err = srv.AddPeer(corebgp.PeerConfig{ 408 | RemoteAddress: remote, 409 | LocalAS: uint32(*localAS), 410 | RemoteAS: uint32(*remoteAS), 411 | }, p, peerOpts...) 412 | if err != nil { 413 | log.Fatalf("error adding peer: %v", err) 414 | } 415 | 416 | srvErrCh := make(chan error) 417 | go func() { 418 | err := srv.Serve([]net.Listener{lis}) 419 | srvErrCh <- err 420 | }() 421 | 422 | sigCh := make(chan os.Signal, 1) 423 | signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) 424 | select { 425 | case sig := <-sigCh: 426 | log.Printf("got signal: %s", sig) 427 | srv.Close() 428 | <-srvErrCh 429 | case err := <-srvErrCh: 430 | log.Fatalf("serve error: %v", err) 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /fsm.go: -------------------------------------------------------------------------------- 1 | package corebgp 2 | 3 | import ( 4 | "context" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "net/netip" 11 | "strconv" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | type fsm struct { 17 | peer *peer 18 | 19 | // the bgp ID received in the latest open message 20 | remoteID uint32 21 | 22 | // conn-related fields 23 | conn net.Conn 24 | dialResultCh chan *dialResult 25 | cancelDialFn context.CancelFunc 26 | 27 | // reader channels 28 | readerMsgCh chan message 29 | readerErrCh chan error 30 | readerDoneCh chan struct{} 31 | closeReaderCh chan struct{} 32 | closeReaderOnce sync.Once 33 | 34 | // control channels 35 | closeOnce sync.Once 36 | closeCh chan struct{} 37 | doneCh chan struct{} 38 | 39 | // timers 40 | connectRetryTimer *time.Timer 41 | holdTimer *time.Timer 42 | holdTime time.Duration 43 | keepAliveTimer *time.Timer 44 | keepAliveInterval time.Duration 45 | idleHoldTimer *time.Timer 46 | } 47 | 48 | func newFSM(peer *peer, conn net.Conn) *fsm { 49 | f := &fsm{ 50 | peer: peer, 51 | conn: conn, 52 | closeCh: make(chan struct{}), 53 | doneCh: make(chan struct{}), 54 | // we do not hold down the first time entering idle state 55 | idleHoldTimer: time.NewTimer(0), 56 | } 57 | return f 58 | } 59 | 60 | type fsmState uint8 61 | 62 | func (f fsmState) String() string { 63 | switch f { 64 | case disabledState: 65 | return "disabled" 66 | case idleState: 67 | return "idle" 68 | case connectState: 69 | return "connect" 70 | case activeState: 71 | return "active" 72 | case openSentState: 73 | return "openSent" 74 | case openConfirmState: 75 | return "openConfirm" 76 | case establishedState: 77 | return "established" 78 | default: 79 | return "unknown" 80 | } 81 | } 82 | 83 | const ( 84 | disabledState fsmState = iota 85 | idleState 86 | connectState 87 | activeState 88 | openSentState 89 | openConfirmState 90 | establishedState 91 | ) 92 | 93 | func (f *fsm) cleanup() { 94 | if f.cancelDialFn != nil { 95 | f.cancelDialFn() 96 | <-f.dialResultCh 97 | } 98 | f.cleanupConnAndReader() 99 | for _, t := range []*time.Timer{f.connectRetryTimer, f.holdTimer, 100 | f.keepAliveTimer, f.idleHoldTimer} { 101 | if t != nil { 102 | t.Stop() 103 | } 104 | } 105 | } 106 | 107 | func (f *fsm) run() { 108 | defer func() { 109 | f.cleanup() 110 | close(f.doneCh) 111 | }() 112 | 113 | var t stateTransition 114 | if f.conn != nil { 115 | // if we start up with a non-nil conn we should enter into the active 116 | // state in order to skip connect and send an open message to the remote 117 | // peer. 118 | t = newStateTransition(disabledState, activeState) 119 | } else { 120 | t = newStateTransition(disabledState, idleState) 121 | } 122 | 123 | for { 124 | // capture target state before peer coordination 125 | toBefore := t.to 126 | 127 | // signal state transition to local peer manager for coordination with 128 | // the "other" fsm. 129 | select { 130 | case f.peer.getFSMTransitionCh(f) <- t: 131 | select { 132 | case <-f.closeCh: 133 | t = newStateTransition(t.from, disabledState) 134 | case t = <-f.peer.getFSMTransitionCh(f): 135 | } 136 | case <-f.closeCh: 137 | t = newStateTransition(t.from, disabledState) 138 | } 139 | 140 | if t.to != toBefore && t.to == disabledState && f.conn != nil && 141 | t.from > activeState { 142 | // we were disabled while transitioning to a target state with an 143 | // active connection 144 | f.sendNotification(newNotification(NOTIF_CODE_CEASE, 0, nil)) // nolint: errcheck 145 | } 146 | 147 | var ( 148 | desired fsmState 149 | err error 150 | ) 151 | switch t.to { 152 | case disabledState: 153 | return 154 | case idleState: 155 | desired = f.idle() 156 | case connectState: 157 | desired = f.connect() 158 | case activeState: 159 | desired = f.active() 160 | case openSentState: 161 | desired, err = f.openSent() 162 | case openConfirmState: 163 | desired, err = f.openConfirm() 164 | case establishedState: 165 | desired, err = f.established() 166 | } 167 | 168 | if err != nil { 169 | // if an error occurred we signal it to the peer 170 | select { 171 | case <-f.closeCh: 172 | t = newStateTransition(t.to, disabledState) 173 | case f.peer.getFSMErrorCh(f) <- err: 174 | t = newStateTransition(t.to, desired) 175 | } 176 | } else { 177 | t = newStateTransition(t.to, desired) 178 | } 179 | } 180 | } 181 | 182 | func (f *fsm) start() { 183 | go f.run() 184 | } 185 | 186 | func (f *fsm) stop() { 187 | f.closeOnce.Do(func() { 188 | close(f.closeCh) 189 | }) 190 | <-f.doneCh 191 | } 192 | 193 | type stateTransition struct { 194 | from fsmState 195 | to fsmState 196 | } 197 | 198 | func newStateTransition(from fsmState, to fsmState) stateTransition { 199 | return stateTransition{ 200 | from: from, 201 | to: to, 202 | } 203 | } 204 | 205 | type dialResult struct { 206 | conn net.Conn 207 | err error 208 | } 209 | 210 | func (f *fsm) dialPeer() { 211 | ctx, cancel := context.WithCancel(context.Background()) 212 | dialResultCh := make(chan *dialResult) 213 | f.dialResultCh = dialResultCh 214 | f.cancelDialFn = cancel 215 | go func() { 216 | defer close(f.dialResultCh) 217 | var ( 218 | laddr net.Addr 219 | err error 220 | ) 221 | if f.peer.options.localAddress.IsValid() { 222 | laddr, err = net.ResolveTCPAddr("tcp", 223 | net.JoinHostPort(f.peer.options.localAddress.String(), "0")) 224 | if err != nil { 225 | dialResultCh <- &dialResult{ 226 | conn: nil, 227 | err: err, 228 | } 229 | } 230 | } 231 | dialer := &net.Dialer{ 232 | LocalAddr: laddr, 233 | Control: f.peer.options.dialerControlFn, 234 | } 235 | conn, err := dialer.DialContext(ctx, "tcp", 236 | net.JoinHostPort(f.peer.config.RemoteAddress.String(), 237 | strconv.Itoa(f.peer.options.port))) 238 | dialResultCh <- &dialResult{ 239 | conn: conn, 240 | err: err, 241 | } 242 | }() 243 | } 244 | 245 | // https://tools.ietf.org/html/rfc4271#section-8.2.2 246 | func (f *fsm) idle() fsmState { 247 | /* 248 | In this state, BGP FSM refuses all incoming BGP connections for 249 | this peer. No resources are allocated to the peer. In response 250 | to a ManualStart event (Event 1) or an AutomaticStart event (Event 251 | 3), the local system: 252 | 253 | - initializes all BGP resources for the peer connection, 254 | - sets ConnectRetryCounter to zero, 255 | - starts the ConnectRetryTimer with the initial value, 256 | - initiates a TCP connection to the other BGP peer, 257 | - listens for a connection that may be initiated by the remote 258 | BGP peer, and 259 | - changes its state to Connect. 260 | 261 | The ManualStop event (Event 2) and AutomaticStop (Event 8) event 262 | are ignored in the Idle state. 263 | */ 264 | select { 265 | case <-f.closeCh: 266 | return disabledState 267 | case <-f.idleHoldTimer.C: 268 | f.connectRetryTimer = time.NewTimer(f.peer.options.connectRetryTime) 269 | f.dialPeer() 270 | f.idleHoldTimer.Reset(f.peer.options.idleHoldTime) 271 | return connectState 272 | } 273 | } 274 | 275 | const ( 276 | // a long hold time is set when transitioning to openSent. 277 | // RFC4271 suggests 4 minutes. 278 | longHoldTime = time.Minute * 4 279 | ) 280 | 281 | func (f *fsm) sendOpenAndSetHoldTimer() fsmState { 282 | capabilities := f.peer.plugin.GetCapabilities(f.peer.config) 283 | o, err := newOpenMessage(f.peer.config.LocalAS, f.peer.options.holdTime, 284 | f.peer.id, capabilities) 285 | if err != nil { 286 | f.conn.Close() 287 | return idleState 288 | } 289 | b, err := o.encode() 290 | if err != nil { 291 | f.conn.Close() 292 | return idleState 293 | } 294 | _, err = f.conn.Write(b) 295 | if err != nil { 296 | f.conn.Close() 297 | return idleState 298 | } 299 | f.holdTimer = time.NewTimer(longHoldTime) 300 | f.startReading() 301 | return openSentState 302 | } 303 | 304 | // https://tools.ietf.org/html/rfc4271#page-54 305 | func (f *fsm) connect() fsmState { 306 | for { 307 | select { 308 | case <-f.closeCh: 309 | f.cancelDialFn() 310 | <-f.dialResultCh 311 | f.connectRetryTimer.Stop() 312 | return disabledState 313 | case dr := <-f.dialResultCh: 314 | if dr.err != nil { 315 | /* 316 | https://tools.ietf.org/html/rfc4271#page-56 317 | If the TCP connection fails (Event 18), the local system checks 318 | the DelayOpenTimer. If the DelayOpenTimer is running, the local 319 | system: [...] 320 | 321 | If the DelayOpenTimer is not running, the local system: 322 | 323 | - stops the ConnectRetryTimer to zero, 324 | - drops the TCP connection, 325 | - releases all BGP resources, and 326 | - changes its state to Idle. 327 | */ 328 | f.connectRetryTimer.Stop() 329 | f.cancelDialFn() 330 | return idleState 331 | } 332 | 333 | /* 334 | https://tools.ietf.org/html/rfc4271#page-55 335 | If the TCP connection succeeds (Event 16 or Event 17), the local 336 | system checks the DelayOpen attribute prior to processing. If the 337 | DelayOpen attribute is set to TRUE, the local system: [...] 338 | 339 | If the DelayOpen attribute is set to FALSE, the local system: 340 | 341 | - stops the ConnectRetryTimer (if running) and sets the 342 | ConnectRetryTimer to zero, 343 | - completes BGP initialization 344 | - sends an OPEN message to its peer, 345 | - sets the HoldTimer to a large value, and 346 | - changes its state to OpenSent. 347 | 348 | A HoldTimer value of 4 minutes is suggested. 349 | */ 350 | f.conn = dr.conn 351 | f.connectRetryTimer.Stop() 352 | return f.sendOpenAndSetHoldTimer() 353 | case <-f.connectRetryTimer.C: 354 | /* 355 | https://tools.ietf.org/html/rfc4271#page-55 356 | In response to the ConnectRetryTimer_Expires event (Event 9), the 357 | local system: 358 | 359 | - drops the TCP connection, 360 | - restarts the ConnectRetryTimer, 361 | - stops the DelayOpenTimer and resets the timer to zero, 362 | - initiates a TCP connection to the other BGP peer, 363 | - continues to listen for a connection that may be initiated by 364 | the remote BGP peer, and 365 | - stays in the Connect state. 366 | */ 367 | f.cancelDialFn() 368 | dr := <-f.dialResultCh 369 | if dr.err != nil { 370 | f.connectRetryTimer = time.NewTimer(f.peer.options.connectRetryTime) 371 | f.dialPeer() 372 | continue 373 | } 374 | // if dr.err == nil we ended up with an established connection 375 | // during the race between connectRetryTimer and the dialer 376 | f.conn = dr.conn 377 | return f.sendOpenAndSetHoldTimer() 378 | } 379 | } 380 | } 381 | 382 | // https://tools.ietf.org/html/rfc4271#page-59 383 | func (f *fsm) active() fsmState { 384 | // if conn is non-nil we were started up with a valid connection as part 385 | // of handling an incoming connection. If conn is nil we are an "outgoing" 386 | // connection FSM 387 | if f.conn != nil { 388 | return f.sendOpenAndSetHoldTimer() 389 | } 390 | 391 | /* 392 | https://tools.ietf.org/html/rfc4271#page-59 393 | In response to a ConnectRetryTimer_Expires event (Event 9), the 394 | local system: 395 | 396 | - restarts the ConnectRetryTimer (with initial value), 397 | - initiates a TCP connection to the other BGP peer, 398 | - continues to listen for a TCP connection that may be initiated 399 | by a remote BGP peer, and 400 | - changes its state to Connect. 401 | */ 402 | select { 403 | case <-f.connectRetryTimer.C: 404 | f.connectRetryTimer = time.NewTimer(f.peer.options.connectRetryTime) 405 | f.dialPeer() 406 | return connectState 407 | case <-f.closeCh: 408 | return disabledState 409 | } 410 | } 411 | 412 | const ( 413 | maxMessageLength = 4096 414 | ) 415 | 416 | func (f *fsm) startReading() { 417 | f.closeReaderCh = make(chan struct{}) 418 | f.closeReaderOnce = sync.Once{} 419 | f.readerDoneCh = make(chan struct{}) 420 | f.readerErrCh = make(chan error) 421 | f.readerMsgCh = make(chan message) 422 | go f.read() 423 | } 424 | 425 | func (f *fsm) cleanupConnAndReader() { 426 | defer func() { 427 | f.conn = nil 428 | }() 429 | if f.conn != nil { 430 | f.conn.Close() 431 | } 432 | if f.closeReaderCh == nil { 433 | return 434 | } 435 | f.closeReaderOnce.Do(func() { 436 | close(f.closeReaderCh) 437 | }) 438 | <-f.readerDoneCh 439 | } 440 | 441 | func (f *fsm) read() { 442 | defer close(f.readerDoneCh) 443 | 444 | for { 445 | header := make([]byte, headerLength) 446 | _, err := io.ReadFull(f.conn, header) 447 | if err != nil { 448 | select { 449 | case <-f.closeReaderCh: 450 | return 451 | case f.readerErrCh <- err: 452 | return 453 | } 454 | } 455 | 456 | for i := 0; i < 16; i++ { 457 | if header[i] != 0xFF { 458 | n := newNotification(NOTIF_CODE_MESSAGE_HEADER_ERR, 459 | NOTIF_SUBCODE_CONN_NOT_SYNCHRONIZED, nil) 460 | select { 461 | case <-f.closeReaderCh: 462 | return 463 | case f.readerErrCh <- newNotificationError(n, true): 464 | return 465 | } 466 | } 467 | } 468 | 469 | // length is inclusive of header 470 | bodyLen := int(binary.BigEndian.Uint16(header[16:18])) - headerLength 471 | if bodyLen < 0 || bodyLen+headerLength > maxMessageLength { 472 | n := newNotification(NOTIF_CODE_MESSAGE_HEADER_ERR, 473 | NOTIF_SUBCODE_BAD_MESSAGE_LEN, nil) 474 | select { 475 | case <-f.closeReaderCh: 476 | return 477 | case f.readerErrCh <- newNotificationError(n, true): 478 | return 479 | } 480 | } 481 | 482 | body := make([]byte, bodyLen) 483 | if bodyLen > 0 { 484 | _, err = io.ReadFull(f.conn, body) 485 | if err != nil { 486 | select { 487 | case <-f.closeReaderCh: 488 | return 489 | case f.readerErrCh <- err: 490 | return 491 | } 492 | } 493 | } 494 | 495 | m, err := messageFromBytes(body, header[18]) 496 | if err != nil { 497 | select { 498 | case <-f.closeReaderCh: 499 | return 500 | case f.readerErrCh <- err: 501 | return 502 | } 503 | } 504 | select { 505 | case <-f.closeReaderCh: 506 | return 507 | case f.readerMsgCh <- m: 508 | } 509 | } 510 | } 511 | 512 | func (f *fsm) sendNotification(n *Notification) error { 513 | b, err := n.encode() 514 | if err != nil { 515 | return err 516 | } 517 | _, err = f.conn.Write(b) 518 | return err 519 | } 520 | 521 | func (f *fsm) sendKeepAlive() error { 522 | k := keepAliveMessage{} 523 | b, err := k.encode() 524 | if err != nil { 525 | return err 526 | } 527 | _, err = f.conn.Write(b) 528 | return err 529 | } 530 | 531 | func (f *fsm) drainAndResetHoldTimer() { 532 | if !f.holdTimer.Stop() { 533 | <-f.holdTimer.C 534 | } 535 | f.holdTimer.Reset(f.holdTime) 536 | } 537 | 538 | // handleNotificationInErr checks if the error unwraps to a notificationError. 539 | // If a notificationError is found and its out field is true, the Notification 540 | // is sent to the peer and the function returns true, otherwise it returns 541 | // false. 542 | func (f *fsm) handleNotificationInErr(err error) bool { 543 | var nerr *notificationError 544 | if errors.As(err, &nerr) && nerr.out { 545 | f.sendNotification(nerr.notification) // nolint: errcheck 546 | return true 547 | } 548 | return false 549 | } 550 | 551 | // https://tools.ietf.org/html/rfc4271#page-63 552 | func (f *fsm) openSent() (fsmState, error) { 553 | openSent := func() (fsmState, error) { 554 | select { 555 | case <-f.closeCh: 556 | n := newNotification(NOTIF_CODE_CEASE, 0, nil) 557 | f.sendNotification(n) // nolint: errcheck 558 | return disabledState, newNotificationError(n, true) 559 | case <-f.holdTimer.C: 560 | /* 561 | https://tools.ietf.org/html/rfc4271#page-64 562 | If the HoldTimer_Expires (Event 10), the local system: 563 | 564 | - sends a NOTIFICATION message with the error code Hold Timer 565 | Expired, 566 | - sets the ConnectRetryTimer to zero, 567 | - releases all BGP resources, 568 | - drops the TCP connection, 569 | - increments the ConnectRetryCounter, 570 | - (optionally) performs peer oscillation damping if the 571 | DampPeerOscillations attribute is set to TRUE, and 572 | - changes its state to Idle. 573 | */ 574 | n := newNotification(NOTIF_CODE_HOLD_TIMER_EXPIRED, 0, nil) 575 | f.sendNotification(n) // nolint: errcheck 576 | return idleState, newNotificationError(n, true) 577 | case err := <-f.readerErrCh: 578 | f.handleNotificationInErr(err) 579 | 580 | var nerr *notificationError 581 | if errors.As(err, &nerr) { 582 | return idleState, fmt.Errorf("reader error: %w", nerr) 583 | } 584 | // if it's not a notificationError, it's connection-related 585 | 586 | /* 587 | https://tools.ietf.org/html/rfc4271#page-64 588 | If a TcpConnectionFails event (Event 18) is received, the local 589 | system: 590 | 591 | - closes the BGP connection, 592 | - restarts the ConnectRetryTimer, 593 | - continues to listen for a connection that may be initiated by 594 | the remote BGP peer, and 595 | - changes its state to Active. 596 | 597 | */ 598 | f.connectRetryTimer = time.NewTimer(f.peer.options.connectRetryTime) 599 | return activeState, fmt.Errorf("reader error: %w", err) 600 | case m := <-f.readerMsgCh: 601 | switch m := m.(type) { 602 | case *Notification: 603 | return idleState, newNotificationError(m, false) 604 | case *openMessage: 605 | /* 606 | https://tools.ietf.org/html/rfc4271#page-65 607 | When an OPEN message is received, all fields are checked for 608 | correctness. If there are no errors in the OPEN message (Event 609 | 19), the local system: 610 | 611 | - resets the DelayOpenTimer to zero, 612 | - sets the BGP ConnectRetryTimer to zero, 613 | - sends a KEEPALIVE message, and 614 | - sets a KeepaliveTimer (via the text below) 615 | - sets the HoldTimer according to the negotiated value (see 616 | Section 4.2), 617 | - changes its state to OpenConfirm. 618 | */ 619 | err := m.validate(f.peer.id, f.peer.config.LocalAS, 620 | f.peer.config.RemoteAS) 621 | if err != nil { 622 | f.handleNotificationInErr(err) 623 | return idleState, fmt.Errorf("error validating open message: %w", err) 624 | } 625 | f.remoteID = m.bgpID 626 | var ridA [4]byte 627 | binary.BigEndian.PutUint32(ridA[:], m.bgpID) 628 | rid := netip.AddrFrom4(ridA) 629 | n := f.peer.plugin.OnOpenMessage(f.peer.config, rid, m.getCapabilities()) 630 | if n != nil { 631 | f.sendNotification(n) // nolint: errcheck 632 | return idleState, newNotificationError(n, true) 633 | } 634 | 635 | err = f.sendKeepAlive() 636 | if err != nil { 637 | return idleState, fmt.Errorf("error sending keepAlive: %w", err) 638 | } 639 | 640 | f.holdTime = time.Duration(m.holdTime) * time.Second 641 | if f.peer.options.holdTime < f.holdTime { 642 | f.holdTime = f.peer.options.holdTime 643 | } 644 | if f.holdTime != 0 { 645 | // https://tools.ietf.org/html/rfc4271#section-4.4 646 | // A reasonable maximum time between KEEPALIVE messages would be one 647 | // third of the Hold Time interval. 648 | f.keepAliveInterval = f.holdTime / 3 649 | f.keepAliveTimer = time.NewTimer(f.keepAliveInterval) 650 | f.drainAndResetHoldTimer() 651 | } 652 | 653 | return openConfirmState, nil 654 | default: 655 | /* 656 | https://tools.ietf.org/html/rfc4271#page-66 657 | In response to any other event (Events 9, 11-13, 20, 25-28), the 658 | local system: 659 | 660 | - sends the NOTIFICATION with the Error Code Finite State 661 | Machine Error, 662 | - sets the ConnectRetryTimer to zero, 663 | - releases all BGP resources, 664 | - drops the TCP connection, 665 | - increments the ConnectRetryCounter by 1, 666 | - (optionally) performs peer oscillation damping if the 667 | DampPeerOscillations attribute is set to TRUE, and 668 | - changes its state to Idle. 669 | 670 | https://tools.ietf.org/html/rfc6608#section-4 671 | If a BGP speaker receives an unexpected message (e.g., KEEPALIVE/ 672 | UPDATE/ROUTE-REFRESH message) on a session in OpenSent state, it MUST 673 | send to the neighbor a NOTIFICATION message with the Error Code 674 | Finite State Machine Error and the Error Subcode "Receive Unexpected 675 | Message in OpenSent State". The Data field is a 1-octet, unsigned 676 | integer that indicates the type of the unexpected message. 677 | */ 678 | n := newNotification(NOTIF_CODE_FSM_ERR, 679 | NOTIF_SUBCODE_RX_UNEXPECTED_MESSAGE_OPENSENT, 680 | []byte{m.messageType()}) 681 | f.sendNotification(n) // nolint: errcheck 682 | return idleState, newNotificationError(n, true) 683 | } 684 | } 685 | } 686 | 687 | to, err := openSent() 688 | if to != openConfirmState { 689 | f.cleanupConnAndReader() 690 | f.holdTimer.Stop() 691 | } 692 | return to, err 693 | } 694 | 695 | // https://tools.ietf.org/html/rfc4271#page-67 696 | func (f *fsm) openConfirm() (fsmState, error) { 697 | openConfirm := func() (fsmState, error) { 698 | for { 699 | select { 700 | case <-f.closeCh: 701 | n := newNotification(NOTIF_CODE_CEASE, 0, nil) 702 | f.sendNotification(n) // nolint: errcheck 703 | return disabledState, newNotificationError(n, true) 704 | case <-f.holdTimer.C: 705 | n := newNotification(NOTIF_CODE_HOLD_TIMER_EXPIRED, 0, nil) 706 | f.sendNotification(n) // nolint: errcheck 707 | return idleState, newNotificationError(n, true) 708 | case <-f.keepAliveTimer.C: 709 | err := f.sendKeepAlive() 710 | if err != nil { 711 | return idleState, fmt.Errorf("error sending keepAlive: %w", err) 712 | } 713 | f.keepAliveTimer.Reset(f.keepAliveInterval) 714 | continue 715 | case err := <-f.readerErrCh: 716 | // In OpenConfirm handling of a TCP connection fails event or 717 | // message decoding error both result in transitioning to Idle. 718 | f.handleNotificationInErr(err) 719 | return idleState, fmt.Errorf("reader error: %w", err) 720 | case m := <-f.readerMsgCh: 721 | switch m := m.(type) { 722 | case *keepAliveMessage: 723 | /* 724 | https://tools.ietf.org/html/rfc4271#page-70 725 | If the local system receives a KEEPALIVE message (KeepAliveMsg 726 | (Event 26)), the local system: 727 | 728 | - restarts the HoldTimer and 729 | - changes its state to Established. 730 | */ 731 | f.drainAndResetHoldTimer() 732 | return establishedState, nil 733 | case *Notification: 734 | return idleState, newNotificationError(m, false) 735 | default: 736 | /* 737 | https://tools.ietf.org/html/rfc4271#page-70 738 | In response to any other event (Events 9, 12-13, 20, 27-28), the 739 | local system: 740 | 741 | - sends a NOTIFICATION with a code of Finite State Machine 742 | Error, 743 | - sets the ConnectRetryTimer to zero, 744 | - releases all BGP resources, 745 | - drops the TCP connection, 746 | - increments the ConnectRetryCounter by 1, 747 | - (optionally) performs peer oscillation damping if the 748 | DampPeerOscillations attribute is set to TRUE, and 749 | - changes its state to Idle. 750 | 751 | https://tools.ietf.org/html/rfc6608#page-3 752 | If a BGP speaker receives an unexpected message (e.g., OPEN/UPDATE/ 753 | ROUTE-REFRESH message) on a session in OpenConfirm state, it MUST 754 | send a NOTIFICATION message with the Error Code Finite State Machine 755 | Error and the Error Subcode "Receive Unexpected Message in 756 | OpenConfirm State" to the neighbor. The Data field is a 1-octet, 757 | unsigned integer that indicates the type of the unexpected message. 758 | */ 759 | n := newNotification(NOTIF_CODE_FSM_ERR, 760 | NOTIF_SUBCODE_RX_UNEXPECTED_MESSAGE_OPENCONFIRM, 761 | []byte{m.messageType()}) 762 | f.sendNotification(n) // nolint: errcheck 763 | return idleState, newNotificationError(n, true) 764 | } 765 | } 766 | } 767 | } 768 | 769 | to, err := openConfirm() 770 | if to != establishedState { 771 | f.cleanupConnAndReader() 772 | f.holdTimer.Stop() 773 | f.keepAliveTimer.Stop() 774 | } 775 | return to, err 776 | } 777 | 778 | type updateMessageWriter struct { 779 | conn net.Conn 780 | resetKATimerCh chan struct{} 781 | closeCh chan struct{} 782 | } 783 | 784 | func (u *updateMessageWriter) WriteUpdate(b []byte) error { 785 | /* 786 | https://tools.ietf.org/html/rfc4271#page-72 787 | Each time the local system sends a KEEPALIVE or UPDATE message, it 788 | restarts its KeepaliveTimer, unless the negotiated HoldTime value 789 | is zero. 790 | */ 791 | select { 792 | case <-u.closeCh: 793 | return io.ErrClosedPipe 794 | default: 795 | _, err := u.conn.Write(prependHeader(b, updateMessageType)) 796 | if err == nil { 797 | select { 798 | case <-u.closeCh: 799 | case u.resetKATimerCh <- struct{}{}: 800 | } 801 | } 802 | return err 803 | } 804 | } 805 | 806 | // https://tools.ietf.org/html/rfc4271#page-71 807 | func (f *fsm) established() (fsmState, error) { 808 | // A separate goroutine is used for resetting the keepAlive timer to 809 | // allow both our main select{} in the established() func below and the 810 | // updateMessageWriter to reset it without synchronizing all input and 811 | // output in the same select{}. Synchronizing all I/O in the same select{} 812 | // would have a negative impact on performance. 813 | kaManagerDoneCh := make(chan struct{}) 814 | closeKAManagerCh := make(chan struct{}) 815 | resetKATimerCh := make(chan struct{}) 816 | go func() { 817 | defer close(kaManagerDoneCh) 818 | for { 819 | select { 820 | case <-closeKAManagerCh: 821 | return 822 | case <-resetKATimerCh: 823 | if f.holdTime != 0 { 824 | f.keepAliveTimer.Reset(f.keepAliveInterval) 825 | } 826 | } 827 | } 828 | }() 829 | 830 | established := func() (fsmState, error) { 831 | writer := &updateMessageWriter{ 832 | conn: f.conn, 833 | resetKATimerCh: resetKATimerCh, 834 | closeCh: make(chan struct{}), 835 | } 836 | defer func() { 837 | close(closeKAManagerCh) 838 | close(writer.closeCh) 839 | }() 840 | handler := f.peer.plugin.OnEstablished(f.peer.config, writer) 841 | 842 | for { 843 | select { 844 | case <-f.closeCh: 845 | n := newNotification(NOTIF_CODE_CEASE, 0, nil) 846 | f.sendNotification(n) // nolint: errcheck 847 | return disabledState, newNotificationError(n, true) 848 | case <-f.holdTimer.C: 849 | n := newNotification(NOTIF_CODE_HOLD_TIMER_EXPIRED, 0, nil) 850 | f.sendNotification(n) // nolint: errcheck 851 | return idleState, newNotificationError(n, true) 852 | case <-f.keepAliveTimer.C: 853 | err := f.sendKeepAlive() 854 | if err != nil { 855 | return idleState, fmt.Errorf("error sending keepAlive: %w", err) 856 | } 857 | resetKATimerCh <- struct{}{} 858 | case err := <-f.readerErrCh: 859 | f.handleNotificationInErr(err) 860 | return idleState, fmt.Errorf("error from reader: %w", err) 861 | case m := <-f.readerMsgCh: 862 | switch m := m.(type) { 863 | case *Notification: 864 | /* 865 | https://tools.ietf.org/html/rfc4271#page-73 866 | If the local system receives a NOTIFICATION message (Event 24 or 867 | Event 25) or a TcpConnectionFails (Event 18) from the underlying 868 | TCP, the local system: 869 | 870 | - sets the ConnectRetryTimer to zero, 871 | - deletes all routes associated with this connection, 872 | - releases all the BGP resources, 873 | - drops the TCP connection, 874 | - increments the ConnectRetryCounter by 1, 875 | - changes its state to Idle. 876 | */ 877 | return idleState, newNotificationError(m, false) 878 | case *keepAliveMessage: 879 | /* 880 | https://tools.ietf.org/html/rfc4271#page-74 881 | If the local system receives a KEEPALIVE message (Event 26), the 882 | local system: 883 | 884 | - restarts its HoldTimer, if the negotiated HoldTime value is 885 | non-zero, and 886 | - remains in the Established state. 887 | */ 888 | if f.holdTime != 0 { 889 | f.drainAndResetHoldTimer() 890 | } 891 | continue 892 | case updateMessage: 893 | /* 894 | If the local system receives an UPDATE message (Event 27), the 895 | local system: 896 | 897 | - processes the message, 898 | - restarts its HoldTimer, if the negotiated HoldTime value is 899 | non-zero, and 900 | - remains in the Established state. 901 | */ 902 | if handler != nil { 903 | n := handler(f.peer.config, m) 904 | if n != nil { 905 | f.sendNotification(n) // nolint: errcheck 906 | return idleState, newNotificationError(n, true) 907 | } 908 | } 909 | if f.holdTime != 0 { 910 | f.drainAndResetHoldTimer() 911 | } 912 | continue 913 | default: 914 | /* 915 | https://tools.ietf.org/html/rfc4271#page-74 916 | In response to any other event (Events 9, 12-13, 20-22), the local 917 | system: 918 | 919 | - sends a NOTIFICATION message with the Error Code Finite State 920 | Machine Error, 921 | - deletes all routes associated with this connection, 922 | - sets the ConnectRetryTimer to zero, 923 | - releases all BGP resources, 924 | - drops the TCP connection, 925 | - increments the ConnectRetryCounter by 1, 926 | - (optionally) performs peer oscillation damping if the 927 | DampPeerOscillations attribute is set to TRUE, and 928 | - changes its state to Idle. 929 | 930 | https://tools.ietf.org/html/rfc6608#page-3 931 | If a BGP speaker receives an unexpected message (e.g., OPEN message) 932 | on a session in Established State, it MUST send to the neighbor a 933 | NOTIFICATION message with the Error Code Finite State Machine Error 934 | and the Error Subcode "Receive Unexpected Message in Established 935 | State". The Data field is a 1-octet, unsigned integer that indicates 936 | the type of the unexpected message. 937 | */ 938 | n := newNotification(NOTIF_CODE_FSM_ERR, 939 | NOTIF_SUBCODE_RX_UNEXPECTED_MESSAGE_ESTABLISHED, 940 | []byte{m.messageType()}) 941 | f.sendNotification(n) // nolint: errcheck 942 | return idleState, newNotificationError(n, true) 943 | } 944 | } 945 | } 946 | } 947 | 948 | to, err := established() 949 | f.cleanupConnAndReader() 950 | f.holdTimer.Stop() 951 | f.keepAliveTimer.Stop() 952 | f.peer.plugin.OnClose(f.peer.config) 953 | return to, err 954 | } 955 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jwhited/corebgp 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/stretchr/testify v1.8.4 7 | golang.org/x/sys v0.15.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 6 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 7 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 8 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /iana_const.go: -------------------------------------------------------------------------------- 1 | // go generate iana_gen.go 2 | // Code generated by the command above; DO NOT EDIT. 3 | 4 | package corebgp 5 | 6 | // Capability Codes, Updated: 2023-01-23 7 | const ( 8 | CAP_MP_EXTENSIONS uint8 = 1 // Multiprotocol Extensions for BGP-4 9 | CAP_ROUTE_REFRESH uint8 = 2 // Route Refresh Capability for BGP-4 10 | CAP_OUTBOUND_ROUTE_FILTERING uint8 = 3 // Outbound Route Filtering Capability 11 | CAP_EXTENDED_NEXT_HOP_ENCODING uint8 = 5 // Extended Next Hop Encoding 12 | CAP_EXTENDED_MESSSAGE uint8 = 6 // BGP Extended Message 13 | CAP_BGPSEC uint8 = 7 // BGPsec Capability 14 | CAP_MULTIPLE_LABELS uint8 = 8 // Multiple Labels Capability 15 | CAP_ROLE uint8 = 9 // BGP Role 16 | CAP_GRACEFUL_RESTART uint8 = 64 // Graceful Restart Capability 17 | CAP_FOUR_OCTET_AS uint8 = 65 // Support for 4-octet AS number capability 18 | CAP_DYNAMIC uint8 = 67 // Support for Dynamic Capability (capability specific) 19 | CAP_MULTISESSION uint8 = 68 // Multisession BGP Capability 20 | CAP_ADD_PATH uint8 = 69 // ADD-PATH Capability 21 | CAP_ENHANCED_ROUTE_REFRESH uint8 = 70 // Enhanced Route Refresh Capability 22 | CAP_LLGR uint8 = 71 // Long-Lived Graceful Restart (LLGR) Capability 23 | CAP_ROUTING_POLICY_DIST uint8 = 72 // Routing Policy Distribution 24 | CAP_FQDN uint8 = 73 // FQDN Capability 25 | CAP_BFD uint8 = 74 // BFD Capability 26 | CAP_SOFTWARE_VERSION uint8 = 75 // Software Version Capability 27 | ) 28 | 29 | // Address Family Numbers, Updated: 2021-10-19 30 | const ( 31 | AFI_IPV4 uint16 = 1 // IP (IP version 4) 32 | AFI_IPV6 uint16 = 2 // IP6 (IP version 6) 33 | AFI_NSAP uint16 = 3 // NSAP 34 | AFI_HDLC uint16 = 4 // HDLC (8-bit multidrop) 35 | AFI_BBN_1822 uint16 = 5 // BBN 1822 36 | AFI_802 uint16 = 6 // 802 (includes all 802 media plus Ethernet "canonical format") 37 | AFI_E163 uint16 = 7 // E.163 38 | AFI_E164 uint16 = 8 // E.164 (SMDS, Frame Relay, ATM) 39 | AFI_F69 uint16 = 9 // F.69 (Telex) 40 | AFI_X121 uint16 = 10 // X.121 (X.25, Frame Relay) 41 | AFI_IPX uint16 = 11 // IPX 42 | AFI_APPLETALK uint16 = 12 // Appletalk 43 | AFI_DECNET_IV uint16 = 13 // Decnet IV 44 | AFI_BANYAN_VINES uint16 = 14 // Banyan Vines 45 | AFI_E164_WITH_NSAP_SUBADDR uint16 = 15 // E.164 with NSAP format subaddress 46 | AFI_DNS uint16 = 16 // DNS (Domain Name System) 47 | AFI_DISTINGUISHED_NAME uint16 = 17 // Distinguished Name 48 | AFI_AS_NUMBER uint16 = 18 // AS Number 49 | AFI_XTP_OVER_IPV4 uint16 = 19 // XTP over IP version 4 50 | AFI_XTP_OVER_IPV6 uint16 = 20 // XTP over IP version 6 51 | AFI_XTP_NATIVE uint16 = 21 // XTP native mode XTP 52 | AFI_FIBRE_CHANNEL_WWPN uint16 = 22 // Fibre Channel World-Wide Port Name 53 | AFI_FIBRE_CHANNEL_WWNN uint16 = 23 // Fibre Channel World-Wide Node Name 54 | AFI_GWID uint16 = 24 // GWID 55 | AFI_L2VPN_INFO uint16 = 25 // AFI for L2VPN information 56 | AFI_MPLS_TP_SECTION_ENDPOINT_ID uint16 = 26 // MPLS-TP Section Endpoint Identifier 57 | AFI_MPLS_TP_LSP_ENDPOINT_ID uint16 = 27 // MPLS-TP LSP Endpoint Identifier 58 | AFI_MPLS_TP_PSEUDOWIRE_ENDPOINT_ID uint16 = 28 // MPLS-TP Pseudowire Endpoint Identifier 59 | AFI_MT_IPV4 uint16 = 29 // MT IP: Multi-Topology IP version 4 60 | AFI_MT_IPV6 uint16 = 30 // MT IPv6: Multi-Topology IP version 6 61 | AFI_BGP_SFC uint16 = 31 // BGP SFC 62 | AFI_EIGRP_COMMON_SERVICE_FAMILY uint16 = 16384 // EIGRP Common Service Family 63 | AFI_EIGRP_IPV4_SERVICE_FAMILY uint16 = 16385 // EIGRP IPv4 Service Family 64 | AFI_EIGRP_IPV6_SERVICE_FAMILY uint16 = 16386 // EIGRP IPv6 Service Family 65 | AFI_LCAF uint16 = 16387 // LISP Canonical Address Format (LCAF) 66 | AFI_BGP_LS uint16 = 16388 // BGP-LS 67 | AFI_48_BIT_MAC uint16 = 16389 // 48-bit MAC 68 | AFI_64_BIT_MAC uint16 = 16390 // 64-bit MAC 69 | AFI_OUI uint16 = 16391 // OUI 70 | AFI_MAC_FINAL_24_BITS uint16 = 16392 // MAC/24 71 | AFI_MAC_FINAL_40_BITS uint16 = 16393 // MAC/40 72 | AFI_IPV6_INITIAL_64_BITS uint16 = 16394 // IPv6/64 73 | AFI_RBRIDGE_PORT_ID uint16 = 16395 // RBridge Port ID 74 | AFI_TRILL_NICKNAME uint16 = 16396 // TRILL Nickname 75 | AFI_UUID uint16 = 16397 // Universally Unique Identifier (UUID) 76 | AFI_ROUTING_POLICY uint16 = 16398 // Routing Policy AFI 77 | AFI_MPLS_NAMESPACES uint16 = 16399 // MPLS Namespaces 78 | ) 79 | 80 | // Subsequent Address Family Identifiers (SAFI) Parameters, Updated: 2022-08-19 81 | const ( 82 | SAFI_UNICAST uint8 = 1 // Network Layer Reachability Information used for unicast forwarding 83 | SAFI_MULTICAST uint8 = 2 // Network Layer Reachability Information used for multicast forwarding 84 | SAFI_MPLS uint8 = 4 // Network Layer Reachability Information (NLRI) with MPLS Labels 85 | SAFI_MCAST_VPN uint8 = 5 // MCAST-VPN 86 | SAFI_DYN_PLACEMENT_MULTI_SEGMENT_PW uint8 = 6 // Network Layer Reachability Information used for Dynamic Placement of Multi-Segment Pseudowires 87 | SAFI_MCAST_VPLS uint8 = 8 // MCAST-VPLS 88 | SAFI_BGP_SFC uint8 = 9 // BGP SFC 89 | SAFI_TUNNEL uint8 = 64 // Tunnel SAFI 90 | SAFI_VPLS uint8 = 65 // Virtual Private LAN Service (VPLS) 91 | SAFI_BGP_MDT uint8 = 66 // BGP MDT SAFI 92 | SAFI_BGP_4OVER6 uint8 = 67 // BGP 4over6 SAFI 93 | SAFI_BGP_6OVER4 uint8 = 68 // BGP 6over4 SAFI 94 | SAFI_LAYER_1_VPN_AUTO_DISCOVERY_INFO uint8 = 69 // Layer-1 VPN auto-discovery information 95 | SAFI_BGP_EVPNS uint8 = 70 // BGP EVPNs 96 | SAFI_BGP_LS uint8 = 71 // BGP-LS 97 | SAFI_BGP_LS_VPN uint8 = 72 // BGP-LS-VPN 98 | SAFI_SR_TE_POLICY uint8 = 73 // SR TE Policy SAFI 99 | SAFI_SD_WAN_CAPABILITIES uint8 = 74 // SD-WAN Capabilities 100 | SAFI_ROUTING_POLICY uint8 = 75 // Routing Policy SAFI 101 | SAFI_CLASSFUL_TRANSPORT uint8 = 76 // Classful-Transport SAFI 102 | SAFI_TUNNELED_TRAFFIC_FLOWSPEC uint8 = 77 // Tunneled Traffic Flowspec 103 | SAFI_MCAST_TREE uint8 = 78 // MCAST-TREE 104 | SAFI_BGP_DPS uint8 = 79 // BGP-DPS (Dynamic Path Selection) 105 | SAFI_BGP_LS_SPF uint8 = 80 // BGP-LS-SPF 106 | SAFI_BGP_CAR uint8 = 83 // BGP CAR 107 | SAFI_BGP_VPN_CAR uint8 = 84 // BGP VPN CAR 108 | SAFI_BGP_MUP uint8 = 85 // BGP-MUP SAFI 109 | SAFI_MPLS_LABELED_VPN_ADDR uint8 = 128 // MPLS-labeled VPN address 110 | SAFI_MULTICAST_BGP_MPLS_IP_VPNS uint8 = 129 // Multicast for BGP/MPLS IP Virtual Private Networks (VPNs) 111 | SAFI_ROUTE_TARGET_CONSTRAINS uint8 = 132 // Route Target constrains 112 | SAFI_DISSEMINATION_OF_FLOWSPEC_RULES uint8 = 133 // Dissemination of Flow Specification rules 113 | SAFI_L3VPN_DISSEMINATION_OF_FLOWSPEC_RULES uint8 = 134 // L3VPN Dissemination of Flow Specification rules 114 | SAFI_VPN_AUTO_DISCOVERY uint8 = 140 // VPN auto-discovery 115 | ) 116 | 117 | // BGP Path Attributes, Updated: 2023-01-18 118 | const ( 119 | PATH_ATTR_ORIGIN uint8 = 1 // ORIGIN 120 | PATH_ATTR_AS_PATH uint8 = 2 // AS_PATH 121 | PATH_ATTR_NEXT_HOP uint8 = 3 // NEXT_HOP 122 | PATH_ATTR_MED uint8 = 4 // MULTI_EXIT_DISC 123 | PATH_ATTR_LOCAL_PREF uint8 = 5 // LOCAL_PREF 124 | PATH_ATTR_ATOMIC_AGGREGATE uint8 = 6 // ATOMIC_AGGREGATE 125 | PATH_ATTR_AGGREGATOR uint8 = 7 // AGGREGATOR 126 | PATH_ATTR_COMMUNITY uint8 = 8 // COMMUNITY 127 | PATH_ATTR_ORIGINATOR_ID uint8 = 9 // ORIGINATOR_ID 128 | PATH_ATTR_CLUSTER_LIST uint8 = 10 // CLUSTER_LIST 129 | PATH_ATTR_MP_REACH_NLRI uint8 = 14 // MP_REACH_NLRI 130 | PATH_ATTR_MP_UNREACH_NLRI uint8 = 15 // MP_UNREACH_NLRI 131 | PATH_ATTR_EXTENDED_COMMUNITIES uint8 = 16 // EXTENDED COMMUNITIES 132 | PATH_ATTR_AS4_PATH uint8 = 17 // AS4_PATH 133 | PATH_ATTR_AS4_AGGREGATOR uint8 = 18 // AS4_AGGREGATOR 134 | PATH_ATTR_PMSI_TUNNEL uint8 = 22 // PMSI_TUNNEL 135 | PATH_ATTR_TUNNEL_ENCAPSULATION uint8 = 23 // Tunnel Encapsulation 136 | PATH_ATTR_TRAFFIC_ENGINEERING uint8 = 24 // Traffic Engineering 137 | PATH_ATTR_IPV6_ADDR_SPECIFIC_EXTENDED_COMMUNITY uint8 = 25 // IPv6 Address Specific Extended Community 138 | PATH_ATTR_AIGP uint8 = 26 // AIGP 139 | PATH_ATTR_PE_DISTINGUISHER_LABELS uint8 = 27 // PE Distinguisher Labels 140 | PATH_ATTR_BGP_LS uint8 = 29 // BGP-LS Attribute 141 | PATH_ATTR_LARGE_COMMUNITY uint8 = 32 // LARGE_COMMUNITY 142 | PATH_ATTR_BGPSEC_PATH uint8 = 33 // BGPsec_Path 143 | PATH_ATTR_OTC uint8 = 35 // Only to Customer (OTC) 144 | PATH_ATTR_SFP_ATTR uint8 = 37 // SFP attribute 145 | PATH_ATTR_BFD_DISCRIMINATOR uint8 = 38 // BFD Discriminator 146 | PATH_ATTR_BGP_PREFIX_SID uint8 = 40 // BGP Prefix-SID 147 | PATH_ATTR_ATTR_SET uint8 = 128 // ATTR_SET 148 | ) 149 | 150 | // BGP Error (Notification) Codes, Updated: 2023-01-18 151 | const ( 152 | NOTIF_CODE_MESSAGE_HEADER_ERR uint8 = 1 // Message Header Error 153 | NOTIF_CODE_OPEN_MESSAGE_ERR uint8 = 2 // OPEN Message Error 154 | NOTIF_CODE_UPDATE_MESSAGE_ERR uint8 = 3 // UPDATE Message Error 155 | NOTIF_CODE_HOLD_TIMER_EXPIRED uint8 = 4 // Hold Timer Expired 156 | NOTIF_CODE_FSM_ERR uint8 = 5 // Finite State Machine Error 157 | NOTIF_CODE_CEASE uint8 = 6 // Cease 158 | NOTIF_CODE_ROUTE_REFRESH_MESSAGE_ERR uint8 = 7 // ROUTE-REFRESH Message Error 159 | ) 160 | 161 | // Message Header Error subcodes, Updated: 2023-01-18 162 | const ( 163 | NOTIF_SUBCODE_CONN_NOT_SYNCHRONIZED uint8 = 1 // Connection Not Synchronized 164 | NOTIF_SUBCODE_BAD_MESSAGE_LEN uint8 = 2 // Bad Message Length 165 | NOTIF_SUBCODE_BAD_MESSAGE_TYPE uint8 = 3 // Bad Message Type 166 | ) 167 | 168 | // OPEN Message Error subcodes, Updated: 2023-01-18 169 | const ( 170 | NOTIF_SUBCODE_UNSUPPORTED_VERSION_NUM uint8 = 1 // Unsupported Version Number 171 | NOTIF_SUBCODE_BAD_PEER_AS uint8 = 2 // Bad Peer AS 172 | NOTIF_SUBCODE_BAD_BGP_ID uint8 = 3 // Bad BGP Identifier 173 | NOTIF_SUBCODE_UNSUPPORTED_OPTIONAL_PARAM uint8 = 4 // Unsupported Optional Parameter 174 | NOTIF_SUBCODE_UNACCEPTABLE_HOLD_TIME uint8 = 6 // Unacceptable Hold Time 175 | NOTIF_SUBCODE_UNSUPPORTED_CAPABILITY uint8 = 7 // Unsupported Capability 176 | NOTIF_SUBCODE_ROLE_MISMATCH uint8 = 11 // Role Mismatch 177 | ) 178 | 179 | // UPDATE Message Error subcodes, Updated: 2023-01-18 180 | const ( 181 | NOTIF_SUBCODE_MALFORMED_ATTR_LIST uint8 = 1 // Malformed Attribute List 182 | NOTIF_SUBCODE_UNRECOGNIZED_WELL_KNOWN_ATTR uint8 = 2 // Unrecognized Well-known Attribute 183 | NOTIF_SUBCODE_MISSING_WELL_KNOWN_ATTR uint8 = 3 // Missing Well-known Attribute 184 | NOTIF_SUBCODE_ATTR_FLAGS_ERR uint8 = 4 // Attribute Flags Error 185 | NOTIF_SUBCODE_ATTR_LEN_ERR uint8 = 5 // Attribute Length Error 186 | NOTIF_SUBCODE_INVALID_ORIGIN_ATTR uint8 = 6 // Invalid ORIGIN Attribute 187 | NOTIF_SUBCODE_INVALID_NEXT_HOP_ATTR uint8 = 8 // Invalid NEXT_HOP Attribute 188 | NOTIF_SUBCODE_OPTIONAL_ATTR_ERR uint8 = 9 // Optional Attribute Error 189 | NOTIF_SUBCODE_INVALID_NETWORK_FIELD uint8 = 10 // Invalid Network Field 190 | NOTIF_SUBCODE_MALFORMED_AS_PATH uint8 = 11 // Malformed AS_PATH 191 | ) 192 | 193 | // BGP Finite State Machine Error Subcodes, Updated: 2023-01-18 194 | const ( 195 | NOTIF_SUBCODE_RX_UNEXPECTED_MESSAGE_OPENSENT uint8 = 1 // Receive Unexpected Message in OpenSent State 196 | NOTIF_SUBCODE_RX_UNEXPECTED_MESSAGE_OPENCONFIRM uint8 = 2 // Receive Unexpected Message in OpenConfirm State 197 | NOTIF_SUBCODE_RX_UNEXPECTED_MESSAGE_ESTABLISHED uint8 = 3 // Receive Unexpected Message in Established State 198 | ) 199 | 200 | // BGP Cease NOTIFICATION message subcodes, Updated: 2023-01-18 201 | const ( 202 | NOTIF_SUBCODE_MAX_NUM_OF_PREFIXES_REACHED uint8 = 1 // Maximum Number of Prefixes Reached 203 | NOTIF_SUBCODE_ADMIN_SHUTDOWN uint8 = 2 // Administrative Shutdown 204 | NOTIF_SUBCODE_PEER_DECONFIGURED uint8 = 3 // Peer De-configured 205 | NOTIF_SUBCODE_ADMIN_RESET uint8 = 4 // Administrative Reset 206 | NOTIF_SUBCODE_CONN_REJECTED uint8 = 5 // Connection Rejected 207 | NOTIF_SUBCODE_OTHER_CONFIG_CHANGE uint8 = 6 // Other Configuration Change 208 | NOTIF_SUBCODE_CONN_COLLISION_RESOLUTION uint8 = 7 // Connection Collision Resolution 209 | NOTIF_SUBCODE_OUT_OF_RESOURCES uint8 = 8 // Out of Resources 210 | NOTIF_SUBCODE_HARD_RESET uint8 = 9 // Hard Reset 211 | NOTIF_SUBCODE_BFD_DOWN uint8 = 10 // BFD Down 212 | ) 213 | 214 | // BGP ROUTE-REFRESH Message Error subcodes, Updated: 2023-01-18 215 | const ( 216 | NOTIF_SUBCODE_INVALID_MESSAGE_LEN uint8 = 1 // Invalid Message Length 217 | ) 218 | 219 | type notifCodeDescAndSubcodes struct { 220 | desc string 221 | subcodes map[uint8]string 222 | } 223 | 224 | var ( 225 | notifCodesMap = map[uint8]notifCodeDescAndSubcodes{ 226 | NOTIF_CODE_MESSAGE_HEADER_ERR: { 227 | desc: "Message Header Error", 228 | subcodes: map[uint8]string{ 229 | NOTIF_SUBCODE_CONN_NOT_SYNCHRONIZED: "Connection Not Synchronized", 230 | NOTIF_SUBCODE_BAD_MESSAGE_LEN: "Bad Message Length", 231 | NOTIF_SUBCODE_BAD_MESSAGE_TYPE: "Bad Message Type", 232 | }, 233 | }, 234 | NOTIF_CODE_OPEN_MESSAGE_ERR: { 235 | desc: "OPEN Message Error", 236 | subcodes: map[uint8]string{ 237 | NOTIF_SUBCODE_UNSUPPORTED_VERSION_NUM: "Unsupported Version Number", 238 | NOTIF_SUBCODE_BAD_PEER_AS: "Bad Peer AS", 239 | NOTIF_SUBCODE_BAD_BGP_ID: "Bad BGP Identifier", 240 | NOTIF_SUBCODE_UNSUPPORTED_OPTIONAL_PARAM: "Unsupported Optional Parameter", 241 | NOTIF_SUBCODE_UNACCEPTABLE_HOLD_TIME: "Unacceptable Hold Time", 242 | NOTIF_SUBCODE_UNSUPPORTED_CAPABILITY: "Unsupported Capability", 243 | NOTIF_SUBCODE_ROLE_MISMATCH: "Role Mismatch", 244 | }, 245 | }, 246 | NOTIF_CODE_UPDATE_MESSAGE_ERR: { 247 | desc: "UPDATE Message Error", 248 | subcodes: map[uint8]string{ 249 | NOTIF_SUBCODE_MALFORMED_ATTR_LIST: "Malformed Attribute List", 250 | NOTIF_SUBCODE_UNRECOGNIZED_WELL_KNOWN_ATTR: "Unrecognized Well-known Attribute", 251 | NOTIF_SUBCODE_MISSING_WELL_KNOWN_ATTR: "Missing Well-known Attribute", 252 | NOTIF_SUBCODE_ATTR_FLAGS_ERR: "Attribute Flags Error", 253 | NOTIF_SUBCODE_ATTR_LEN_ERR: "Attribute Length Error", 254 | NOTIF_SUBCODE_INVALID_ORIGIN_ATTR: "Invalid ORIGIN Attribute", 255 | NOTIF_SUBCODE_INVALID_NEXT_HOP_ATTR: "Invalid NEXT_HOP Attribute", 256 | NOTIF_SUBCODE_OPTIONAL_ATTR_ERR: "Optional Attribute Error", 257 | NOTIF_SUBCODE_INVALID_NETWORK_FIELD: "Invalid Network Field", 258 | NOTIF_SUBCODE_MALFORMED_AS_PATH: "Malformed AS_PATH", 259 | }, 260 | }, 261 | NOTIF_CODE_HOLD_TIMER_EXPIRED: { 262 | desc: "Hold Timer Expired", 263 | subcodes: map[uint8]string{}, 264 | }, 265 | NOTIF_CODE_FSM_ERR: { 266 | desc: "Finite State Machine Error", 267 | subcodes: map[uint8]string{ 268 | NOTIF_SUBCODE_RX_UNEXPECTED_MESSAGE_OPENSENT: "Receive Unexpected Message in OpenSent State", 269 | NOTIF_SUBCODE_RX_UNEXPECTED_MESSAGE_OPENCONFIRM: "Receive Unexpected Message in OpenConfirm State", 270 | NOTIF_SUBCODE_RX_UNEXPECTED_MESSAGE_ESTABLISHED: "Receive Unexpected Message in Established State", 271 | }, 272 | }, 273 | NOTIF_CODE_CEASE: { 274 | desc: "Cease", 275 | subcodes: map[uint8]string{ 276 | NOTIF_SUBCODE_MAX_NUM_OF_PREFIXES_REACHED: "Maximum Number of Prefixes Reached", 277 | NOTIF_SUBCODE_ADMIN_SHUTDOWN: "Administrative Shutdown", 278 | NOTIF_SUBCODE_PEER_DECONFIGURED: "Peer De-configured", 279 | NOTIF_SUBCODE_ADMIN_RESET: "Administrative Reset", 280 | NOTIF_SUBCODE_CONN_REJECTED: "Connection Rejected", 281 | NOTIF_SUBCODE_OTHER_CONFIG_CHANGE: "Other Configuration Change", 282 | NOTIF_SUBCODE_CONN_COLLISION_RESOLUTION: "Connection Collision Resolution", 283 | NOTIF_SUBCODE_OUT_OF_RESOURCES: "Out of Resources", 284 | NOTIF_SUBCODE_HARD_RESET: "Hard Reset", 285 | NOTIF_SUBCODE_BFD_DOWN: "BFD Down", 286 | }, 287 | }, 288 | NOTIF_CODE_ROUTE_REFRESH_MESSAGE_ERR: { 289 | desc: "ROUTE-REFRESH Message Error", 290 | subcodes: map[uint8]string{ 291 | NOTIF_SUBCODE_INVALID_MESSAGE_LEN: "Invalid Message Length", 292 | }, 293 | }, 294 | } 295 | ) 296 | -------------------------------------------------------------------------------- /iana_gen.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | // +build ignore 3 | 4 | //go:generate go run iana_gen.go 5 | 6 | // this program generates BGP-relevant IANA constants by reading IANA 7 | // registries. Original inspiration for this generation technique stemmed from 8 | // golang.org/x/net/internal/iana 9 | // 10 | // Generating allows us to more easily keep up to date with new RFCs, and 11 | // removes the human error factor. 12 | package main 13 | 14 | import ( 15 | "bytes" 16 | "encoding/xml" 17 | "flag" 18 | "fmt" 19 | "go/format" 20 | "io" 21 | "net/http" 22 | "os" 23 | "sort" 24 | "strconv" 25 | "strings" 26 | "time" 27 | ) 28 | 29 | var registries = []struct { 30 | url string 31 | parseFn func(io.Writer, io.Reader) error 32 | }{ 33 | { 34 | url: "https://www.iana.org/assignments/capability-codes/capability-codes.xml", 35 | parseFn: parseCapabilityRegistry, 36 | }, 37 | { 38 | url: "https://www.iana.org/assignments/address-family-numbers/address-family-numbers.xml", 39 | parseFn: parseAFIRegistry, 40 | }, 41 | { 42 | url: "https://www.iana.org/assignments/safi-namespace/safi-namespace.xml", 43 | parseFn: parseSAFIRegistry, 44 | }, 45 | { 46 | url: "https://www.iana.org/assignments/bgp-parameters/bgp-parameters.xml", 47 | parseFn: parseBGPParametersRegistry, 48 | }, 49 | } 50 | 51 | var ( 52 | flagPrint = flag.Bool("print", false, "print unformatted generated code to stdout and then exit") 53 | ) 54 | 55 | func main() { 56 | flag.Parse() 57 | var buf bytes.Buffer 58 | buf.WriteString("// go generate iana_gen.go\n") 59 | buf.WriteString("// Code generated by the command above; DO NOT EDIT.\n\n") 60 | buf.WriteString("package corebgp\n\n") 61 | client := http.Client{ 62 | Timeout: time.Second * 10, 63 | } 64 | for _, r := range registries { 65 | resp, err := client.Get(r.url) 66 | if err != nil { 67 | fmt.Fprintf(os.Stderr, "error retrieving %s: %v\n", r.url, err) 68 | os.Exit(1) 69 | } 70 | defer resp.Body.Close() 71 | if resp.StatusCode != http.StatusOK { 72 | fmt.Fprintf(os.Stderr, "got non-200 status (%d) for %s\n", 73 | resp.StatusCode, r.url) 74 | os.Exit(1) 75 | } 76 | err = r.parseFn(&buf, resp.Body) 77 | if err != nil { 78 | fmt.Fprintf(os.Stderr, "error parsing resp from %s: %v\n", r.url, 79 | err) 80 | os.Exit(1) 81 | } 82 | buf.WriteString("\n") 83 | } 84 | if *flagPrint { 85 | fmt.Println(string(buf.Bytes())) 86 | os.Exit(0) 87 | } 88 | b, err := format.Source(buf.Bytes()) 89 | if err != nil { 90 | fmt.Fprintf(os.Stderr, "error formatting source: %v\n", err) 91 | os.Exit(1) 92 | } 93 | err = os.WriteFile("iana_const.go", b, 0644) 94 | if err != nil { 95 | fmt.Fprintf(os.Stderr, "error writing file: %v\n", err) 96 | os.Exit(1) 97 | } 98 | } 99 | 100 | type constRecord struct { 101 | originalName string 102 | name string 103 | value int 104 | } 105 | 106 | type capabilityRegistry struct { 107 | XMLName xml.Name `xml:"registry"` 108 | Title string `xml:"title"` 109 | Updated string `xml:"updated"` 110 | Registries []struct { 111 | Title string `xml:"title"` 112 | Records []struct { 113 | Value string `xml:"value"` 114 | Description string `xml:"description"` 115 | } `xml:"record"` 116 | } `xml:"registry"` 117 | } 118 | 119 | func (c *capabilityRegistry) escape() []constRecord { 120 | constRecords := make([]constRecord, 0) 121 | for _, registry := range c.Registries { 122 | if registry.Title != "Capability Codes" { 123 | continue 124 | } 125 | sr := strings.NewReplacer( 126 | " for BGP-4", "", 127 | " Capability", "", 128 | " ", "_", 129 | "-", "_", 130 | ) 131 | for _, record := range registry.Records { 132 | if strings.Contains(record.Description, "Reserved") || 133 | strings.Contains(record.Description, "deprecated") || 134 | strings.Contains(record.Description, "Deprecated") { 135 | continue 136 | } 137 | value, err := strconv.ParseUint(record.Value, 10, 8) 138 | if err != nil { 139 | continue 140 | } 141 | cr := constRecord{ 142 | originalName: record.Description, 143 | value: int(value), 144 | } 145 | s := record.Description 146 | switch s { 147 | case "Multiprotocol Extensions for BGP-4": 148 | cr.name = "MP_EXTENSIONS" 149 | case "BGP Extended Message": 150 | cr.name = "EXTENDED_MESSSAGE" 151 | case "BGP Role": 152 | cr.name = "ROLE" 153 | case "Support for 4-octet AS number capability": 154 | cr.name = "FOUR_OCTET_AS" 155 | case "Support for Dynamic Capability (capability specific)": 156 | cr.name = "DYNAMIC" 157 | case "Multisession BGP Capability": 158 | cr.name = "MULTISESSION" 159 | case "Long-Lived Graceful Restart (LLGR) Capability": 160 | cr.name = "LLGR" 161 | case "Routing Policy Distribution": 162 | cr.name = "ROUTING_POLICY_DIST" 163 | default: 164 | s = strings.TrimSpace(s) 165 | s = sr.Replace(s) 166 | cr.name = strings.ToUpper(s) 167 | } 168 | constRecords = append(constRecords, cr) 169 | } 170 | } 171 | return constRecords 172 | } 173 | 174 | func parseCapabilityRegistry(w io.Writer, r io.Reader) error { 175 | c := capabilityRegistry{} 176 | dec := xml.NewDecoder(r) 177 | err := dec.Decode(&c) 178 | if err != nil { 179 | return err 180 | } 181 | fmt.Fprintf(w, "// %s, Updated: %s\n", c.Title, c.Updated) 182 | fmt.Fprint(w, "const(\n") 183 | for _, cr := range c.escape() { 184 | fmt.Fprintf(w, "CAP_%s uint8 = %d", cr.name, cr.value) 185 | fmt.Fprintf(w, "// %s\n", cr.originalName) 186 | } 187 | fmt.Fprint(w, ")\n") 188 | return nil 189 | } 190 | 191 | type afiRegistry struct { 192 | XMLName xml.Name `xml:"registry"` 193 | Title string `xml:"title"` 194 | Updated string `xml:"updated"` 195 | Registry struct { 196 | Title string `xml:"title"` 197 | Records []struct { 198 | Value string `xml:"value"` 199 | Description string `xml:"description"` 200 | } `xml:"record"` 201 | } `xml:"registry"` 202 | } 203 | 204 | func (a *afiRegistry) escape() []constRecord { 205 | constRecords := make([]constRecord, 0) 206 | sr := strings.NewReplacer( 207 | "Identifier", "ID", 208 | " ", "_", 209 | ".", "", 210 | "-", "_", 211 | ) 212 | for _, record := range a.Registry.Records { 213 | if strings.Contains(record.Description, "Reserved") || 214 | strings.Contains(record.Description, "Unassigned") { 215 | continue 216 | } 217 | value, err := strconv.ParseUint(record.Value, 10, 16) 218 | if err != nil { 219 | continue 220 | } 221 | cr := constRecord{ 222 | originalName: record.Description, 223 | value: int(value), 224 | } 225 | s := record.Description 226 | switch s { 227 | case "IP (IP version 4)": 228 | cr.name = "IPV4" 229 | case "IP6 (IP version 6)": 230 | cr.name = "IPV6" 231 | case "E.164 with NSAP format subaddress": 232 | cr.name = "E164_WITH_NSAP_SUBADDR" 233 | case "XTP over IP version 4": 234 | cr.name = "XTP_OVER_IPV4" 235 | case "XTP over IP version 6": 236 | cr.name = "XTP_OVER_IPV6" 237 | case "XTP native mode XTP": 238 | cr.name = "XTP_NATIVE" 239 | case "Fibre Channel World-Wide Port Name": 240 | cr.name = "FIBRE_CHANNEL_WWPN" 241 | case "Fibre Channel World-Wide Node Name": 242 | cr.name = "FIBRE_CHANNEL_WWNN" 243 | case "AFI for L2VPN information": 244 | cr.name = "L2VPN_INFO" 245 | case "MT IP: Multi-Topology IP version 4": 246 | cr.name = "MT_IPV4" 247 | case "MT IPv6: Multi-Topology IP version 6": 248 | cr.name = "MT_IPV6" 249 | case "LISP Canonical Address Format (LCAF)": 250 | cr.name = "LCAF" 251 | case "MAC/24": 252 | cr.name = "MAC_FINAL_24_BITS" 253 | case "MAC/40": 254 | cr.name = "MAC_FINAL_40_BITS" 255 | case "IPv6/64": 256 | cr.name = "IPV6_INITIAL_64_BITS" 257 | case "Routing Policy AFI": 258 | cr.name = "ROUTING_POLICY" 259 | case "Universally Unique Identifier (UUID)": 260 | cr.name = "UUID" 261 | default: 262 | n := strings.Index(s, "(") 263 | if n > 0 { 264 | s = s[:n] 265 | } 266 | n = strings.Index(s, ":") 267 | if n > 0 { 268 | s = s[:n] 269 | } 270 | s = strings.TrimSpace(s) 271 | s = sr.Replace(s) 272 | cr.name = strings.ToUpper(s) 273 | } 274 | constRecords = append(constRecords, cr) 275 | } 276 | return constRecords 277 | } 278 | 279 | func parseAFIRegistry(w io.Writer, r io.Reader) error { 280 | a := afiRegistry{} 281 | dec := xml.NewDecoder(r) 282 | err := dec.Decode(&a) 283 | if err != nil { 284 | return err 285 | } 286 | fmt.Fprintf(w, "// %s, Updated: %s\n", a.Title, a.Updated) 287 | fmt.Fprint(w, "const(\n") 288 | for _, afc := range a.escape() { 289 | fmt.Fprintf(w, "AFI_%s uint16 = %d", afc.name, afc.value) 290 | fmt.Fprintf(w, "// %s\n", afc.originalName) 291 | } 292 | fmt.Fprint(w, ")\n") 293 | return nil 294 | } 295 | 296 | type safiRegistry struct { 297 | XMLName xml.Name `xml:"registry"` 298 | Title string `xml:"title"` 299 | Updated string `xml:"updated"` 300 | Registry struct { 301 | Title string `xml:"title"` 302 | Records []struct { 303 | Value string `xml:"value"` 304 | Description string `xml:"description"` 305 | } `xml:"record"` 306 | } `xml:"registry"` 307 | } 308 | 309 | func (a *safiRegistry) escape() []constRecord { 310 | constRecords := make([]constRecord, 0) 311 | sr := strings.NewReplacer( 312 | " SAFI", "", 313 | "Flow Specification", "FLOWSPEC", 314 | " ", "_", 315 | ".", "", 316 | "-", "_", 317 | "/", "", 318 | ) 319 | for _, record := range a.Registry.Records { 320 | if strings.Contains(record.Description, "Reserved") || 321 | strings.Contains(record.Description, "Unassigned") || 322 | strings.Contains(record.Description, "OBSOLETE") { 323 | continue 324 | } 325 | value, err := strconv.ParseUint(record.Value, 10, 8) 326 | if err != nil { 327 | continue 328 | } 329 | cr := constRecord{ 330 | originalName: record.Description, 331 | value: int(value), 332 | } 333 | s := record.Description 334 | switch s { 335 | case "Network Layer Reachability Information used \nfor unicast forwarding": 336 | cr.originalName = "Network Layer Reachability Information used for unicast forwarding" 337 | cr.name = "UNICAST" 338 | case "Network Layer Reachability Information used \nfor multicast forwarding": 339 | cr.originalName = "Network Layer Reachability Information used for multicast forwarding" 340 | cr.name = "MULTICAST" 341 | case "Network Layer Reachability Information (NLRI) \nwith MPLS Labels": 342 | cr.originalName = "Network Layer Reachability Information (NLRI) with MPLS Labels" 343 | cr.name = "MPLS" 344 | case "Network Layer Reachability Information used \nfor Dynamic Placement of Multi-Segment Pseudowires": 345 | cr.originalName = "Network Layer Reachability Information used for Dynamic Placement of Multi-Segment Pseudowires" 346 | cr.name = "DYN_PLACEMENT_MULTI_SEGMENT_PW" 347 | case "Virtual Private LAN Service (VPLS)": 348 | cr.name = "VPLS" 349 | case "Layer-1 VPN auto-discovery information": 350 | cr.name = "LAYER_1_VPN_AUTO_DISCOVERY_INFO" 351 | case "MPLS-labeled VPN address": 352 | cr.name = "MPLS_LABELED_VPN_ADDR" 353 | case "Multicast for BGP/MPLS IP Virtual Private \nNetworks (VPNs)": 354 | cr.originalName = "Multicast for BGP/MPLS IP Virtual Private Networks (VPNs)" 355 | cr.name = "MULTICAST_BGP_MPLS_IP_VPNS" 356 | default: 357 | n := strings.Index(s, "(") 358 | if n > 0 { 359 | s = s[:n] 360 | } 361 | n = strings.Index(s, ":") 362 | if n > 0 { 363 | s = s[:n] 364 | } 365 | s = strings.TrimSpace(s) 366 | cr.name = strings.ToUpper(sr.Replace(s)) 367 | } 368 | constRecords = append(constRecords, cr) 369 | } 370 | return constRecords 371 | } 372 | 373 | func parseSAFIRegistry(w io.Writer, r io.Reader) error { 374 | s := safiRegistry{} 375 | dec := xml.NewDecoder(r) 376 | err := dec.Decode(&s) 377 | if err != nil { 378 | return err 379 | } 380 | fmt.Fprintf(w, "// %s, Updated: %s\n", s.Title, s.Updated) 381 | fmt.Fprint(w, "const(\n") 382 | for _, cr := range s.escape() { 383 | fmt.Fprintf(w, "SAFI_%s uint8 = %d", cr.name, cr.value) 384 | fmt.Fprintf(w, "// %s\n", cr.originalName) 385 | } 386 | fmt.Fprint(w, ")\n") 387 | return nil 388 | } 389 | 390 | type bgpParametersRegistry struct { 391 | XMLName xml.Name `xml:"registry"` 392 | Title string `xml:"title"` 393 | Updated string `xml:"updated"` 394 | Registries []struct { 395 | Title string `xml:"title"` 396 | Records []bgpParametersRecord `xml:"record"` 397 | Registries []struct { 398 | Title string `xml:"title"` 399 | Records []bgpParametersRecord `xml:"record"` 400 | } `xml:"registry"` 401 | } `xml:"registry"` 402 | } 403 | 404 | type bgpParametersRecord struct { 405 | Value string `xml:"value"` 406 | Name string `xml:"name"` 407 | Code string `xml:"code"` 408 | } 409 | 410 | type bgpParametersBlock struct { 411 | title string 412 | records []constRecord 413 | } 414 | 415 | func escapePathAttributes(records []bgpParametersRecord) []constRecord { 416 | constRecords := make([]constRecord, 0) 417 | sr := strings.NewReplacer( 418 | " ", "_", 419 | "-", "_", 420 | "attribute", "ATTR", 421 | ) 422 | for _, record := range records { 423 | if strings.Contains(record.Code, "Reserved") || 424 | strings.Contains(record.Code, "deprecated") || 425 | strings.Contains(record.Code, "Deprecated") || 426 | strings.Contains(record.Code, "TEMPORARY") || 427 | strings.Contains(record.Code, "Unassigned") { 428 | continue 429 | } 430 | value, err := strconv.ParseUint(record.Value, 10, 8) 431 | if err != nil { 432 | continue 433 | } 434 | cr := constRecord{ 435 | originalName: record.Code, 436 | value: int(value), 437 | } 438 | s := record.Code 439 | switch s { 440 | case "MULTI_EXIT_DISC": 441 | cr.name = "MED" 442 | case "IPv6 Address Specific Extended Community": 443 | cr.name = "IPV6_ADDR_SPECIFIC_EXTENDED_COMMUNITY" 444 | case "BGP-LS Attribute": 445 | cr.name = "BGP_LS" 446 | case "Only to Customer (OTC)": 447 | cr.name = "OTC" 448 | default: 449 | s = sr.Replace(s) 450 | cr.name = strings.ToUpper(s) 451 | } 452 | cr.name = "PATH_ATTR_" + cr.name 453 | constRecords = append(constRecords, cr) 454 | } 455 | return constRecords 456 | } 457 | 458 | func escapeNotificationCodes(records []bgpParametersRecord) []constRecord { 459 | constRecords := make([]constRecord, 0) 460 | sr := strings.NewReplacer( 461 | " ", "_", 462 | "-", "_", 463 | "Error", "ERR", 464 | "Finite State Machine", "FSM", 465 | ) 466 | for _, record := range records { 467 | if strings.Contains(record.Name, "Reserved") || 468 | strings.Contains(record.Name, "Unassigned") { 469 | continue 470 | } 471 | value, err := strconv.ParseUint(record.Value, 10, 8) 472 | if err != nil { 473 | continue 474 | } 475 | cr := constRecord{ 476 | originalName: record.Name, 477 | value: int(value), 478 | } 479 | s := record.Name 480 | switch s { 481 | default: 482 | s = sr.Replace(s) 483 | cr.name = strings.ToUpper(s) 484 | } 485 | cr.name = "NOTIF_CODE_" + cr.name 486 | constRecords = append(constRecords, cr) 487 | } 488 | return constRecords 489 | } 490 | 491 | func escapeNotificationSubcodes(records []bgpParametersRecord) []constRecord { 492 | constRecords := make([]constRecord, 0) 493 | sr := strings.NewReplacer( 494 | " ", "_", 495 | "-", "_", 496 | "Error", "ERR", 497 | "Connection", "CONN", 498 | "Configuration", "CONFIG", 499 | "Maximum", "MAX", 500 | "Number", "NUM", 501 | "Identifier", "ID", 502 | "Attribute", "ATTR", 503 | "Parameter", "PARAM", 504 | "Administrative", "ADMIN", 505 | "Length", "LEN", 506 | ) 507 | for _, record := range records { 508 | if strings.Contains(record.Name, "Reserved") || 509 | strings.Contains(record.Name, "Unassigned") || 510 | strings.Contains(record.Name, "Deprecated") || 511 | strings.Contains(record.Name, "Unspecific") || 512 | strings.Contains(record.Name, "Unspecified Error") || 513 | strings.Contains(record.Name, "TEMPORARY") { 514 | continue 515 | } 516 | value, err := strconv.ParseUint(record.Value, 10, 8) 517 | if err != nil { 518 | continue 519 | } 520 | cr := constRecord{ 521 | originalName: record.Name, 522 | value: int(value), 523 | } 524 | s := record.Name 525 | switch s { 526 | case "Receive Unexpected Message in OpenSent State": 527 | cr.name = "RX_UNEXPECTED_MESSAGE_OPENSENT" 528 | case "Receive Unexpected Message in OpenConfirm State": 529 | cr.name = "RX_UNEXPECTED_MESSAGE_OPENCONFIRM" 530 | case "Receive Unexpected Message in Established State": 531 | cr.name = "RX_UNEXPECTED_MESSAGE_ESTABLISHED" 532 | case "Peer De-configured": 533 | cr.name = "PEER_DECONFIGURED" 534 | default: 535 | s = sr.Replace(s) 536 | cr.name = strings.ToUpper(s) 537 | } 538 | cr.name = "NOTIF_SUBCODE_" + cr.name 539 | constRecords = append(constRecords, cr) 540 | } 541 | return constRecords 542 | } 543 | 544 | type notifCodeAndSubcodes struct { 545 | constRecord 546 | subcodes []constRecord 547 | } 548 | 549 | func (a *bgpParametersRegistry) escape() ([]bgpParametersBlock, 550 | []notifCodeAndSubcodes) { 551 | bgpParametersBlocks := make([]bgpParametersBlock, 0) 552 | codes := make(map[int]*notifCodeAndSubcodes) 553 | for _, registry := range a.Registries { 554 | switch registry.Title { 555 | case "BGP Path Attributes": 556 | b := bgpParametersBlock{ 557 | title: registry.Title, 558 | } 559 | b.records = escapePathAttributes(registry.Records) 560 | bgpParametersBlocks = append(bgpParametersBlocks, b) 561 | case "BGP Error (Notification) Codes": 562 | b := bgpParametersBlock{ 563 | title: registry.Title, 564 | } 565 | b.records = escapeNotificationCodes(registry.Records) 566 | bgpParametersBlocks = append(bgpParametersBlocks, b) 567 | for _, record := range b.records { 568 | codes[record.value] = ¬ifCodeAndSubcodes{ 569 | constRecord: record, 570 | subcodes: make([]constRecord, 0), 571 | } 572 | } 573 | case "BGP Error Subcodes": 574 | var i = 1 575 | for _, innerRegistry := range registry.Registries { 576 | b := bgpParametersBlock{ 577 | title: innerRegistry.Title, 578 | } 579 | b.records = escapeNotificationSubcodes(innerRegistry.Records) 580 | bgpParametersBlocks = append(bgpParametersBlocks, b) 581 | c := codes[i] 582 | for _, record := range b.records { 583 | c.subcodes = append(c.subcodes, record) 584 | } 585 | if i == 3 { 586 | // skip 4, hold timer expired has no subcodes 587 | i += 2 588 | } else { 589 | i++ 590 | } 591 | } 592 | default: 593 | continue 594 | } 595 | } 596 | sorted := make([]notifCodeAndSubcodes, 0) 597 | for _, v := range codes { 598 | sorted = append(sorted, *v) 599 | } 600 | sort.Slice(sorted, func(i, j int) bool { 601 | return sorted[i].value < sorted[j].value 602 | }) 603 | return bgpParametersBlocks, sorted 604 | } 605 | 606 | func parseBGPParametersRegistry(w io.Writer, r io.Reader) error { 607 | b := bgpParametersRegistry{} 608 | dec := xml.NewDecoder(r) 609 | err := dec.Decode(&b) 610 | if err != nil { 611 | return err 612 | } 613 | blocks, codes := b.escape() 614 | for _, block := range blocks { 615 | fmt.Fprintf(w, "// %s, Updated: %s\n", block.title, b.Updated) 616 | fmt.Fprint(w, "const(\n") 617 | for _, cr := range block.records { 618 | fmt.Fprintf(w, "%s uint8 = %d", cr.name, cr.value) 619 | fmt.Fprintf(w, "// %s\n", cr.originalName) 620 | } 621 | fmt.Fprint(w, ")\n") 622 | } 623 | fmt.Fprint(w, "type notifCodeDescAndSubcodes struct {\n") 624 | fmt.Fprint(w, "desc string\n") 625 | fmt.Fprint(w, "subcodes map[uint8]string\n") 626 | fmt.Fprint(w, "}\n") 627 | fmt.Fprint(w, "var (\n") 628 | fmt.Fprint(w, "notifCodesMap = map[uint8]notifCodeDescAndSubcodes{\n") 629 | for _, code := range codes { 630 | fmt.Fprintf(w, "%s: {\n", code.name) 631 | fmt.Fprintf(w, `desc: "%s",`+"\n", code.originalName) 632 | fmt.Fprint(w, "subcodes: map[uint8]string{\n") 633 | for _, subcode := range code.subcodes { 634 | fmt.Fprintf(w, `%s: "%s",`+"\n", subcode.name, subcode.originalName) 635 | } 636 | fmt.Fprint(w, "},\n") 637 | fmt.Fprint(w, "},\n") 638 | } 639 | fmt.Fprint(w, "}\n") 640 | fmt.Fprint(w, ")\n") 641 | return nil 642 | } 643 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package corebgp 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Logger is a log.Print-compatible function 8 | type Logger func(...interface{}) 9 | 10 | var ( 11 | logger Logger = nil 12 | ) 13 | 14 | // SetLogger enables logging with the provided Logger. 15 | func SetLogger(l Logger) { 16 | logger = l 17 | } 18 | 19 | func log(v ...interface{}) { 20 | if logger != nil { 21 | logger(v...) 22 | } 23 | } 24 | 25 | func logf(format string, v ...interface{}) { 26 | log(fmt.Sprintf(format, v...)) 27 | } 28 | -------------------------------------------------------------------------------- /notification_error.go: -------------------------------------------------------------------------------- 1 | package corebgp 2 | 3 | import "fmt" 4 | 5 | type notificationError struct { 6 | notification *Notification 7 | out bool 8 | } 9 | 10 | func newNotificationError(n *Notification, out bool) *notificationError { 11 | return ¬ificationError{ 12 | notification: n, 13 | out: out, 14 | } 15 | } 16 | 17 | func (n *notificationError) dampPeer() bool { 18 | return n.notification.Code != NOTIF_CODE_CEASE 19 | } 20 | 21 | func (n *notificationError) Error() string { 22 | direction := "received" 23 | if n.out { 24 | direction = "sent" 25 | } 26 | var codeDesc, subcodeDesc string 27 | d, ok := notifCodesMap[n.notification.Code] 28 | if ok { 29 | codeDesc = d.desc 30 | s, ok := d.subcodes[n.notification.Subcode] 31 | if ok { 32 | subcodeDesc = s 33 | } 34 | } 35 | return fmt.Sprintf("notification %s code: %d (%s) subcode: %d (%s)", 36 | direction, n.notification.Code, codeDesc, n.notification.Subcode, 37 | subcodeDesc) 38 | } 39 | -------------------------------------------------------------------------------- /packet.go: -------------------------------------------------------------------------------- 1 | package corebgp 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "math" 9 | "net/netip" 10 | "time" 11 | ) 12 | 13 | const ( 14 | openMessageType = 1 15 | updateMessageType = 2 16 | notificationMessageType = 3 17 | keepAliveMessageType = 4 18 | ) 19 | 20 | type message interface { 21 | messageType() uint8 22 | } 23 | 24 | const ( 25 | headerLength = 19 26 | ) 27 | 28 | func messageFromBytes(b []byte, messageType uint8) (message, error) { 29 | switch messageType { 30 | case openMessageType: 31 | o := &openMessage{} 32 | err := o.decode(b) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return o, nil 37 | case updateMessageType: 38 | u := make([]byte, len(b)) 39 | copy(u, b) 40 | return updateMessage(u), nil 41 | case notificationMessageType: 42 | n := &Notification{} 43 | err := n.decode(b) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return n, nil 48 | case keepAliveMessageType: 49 | k := &keepAliveMessage{} 50 | return k, nil 51 | default: 52 | badType := make([]byte, 1) 53 | badType[0] = messageType 54 | n := newNotification(NOTIF_CODE_MESSAGE_HEADER_ERR, 55 | NOTIF_SUBCODE_BAD_MESSAGE_TYPE, badType) 56 | return nil, newNotificationError(n, true) 57 | } 58 | } 59 | 60 | func prependHeader(m []byte, t uint8) []byte { 61 | b := make([]byte, headerLength) 62 | for i := 0; i < 16; i++ { 63 | b[i] = 0xFF 64 | } 65 | msgLen := uint16(len(m) + headerLength) 66 | binary.BigEndian.PutUint16(b[16:], msgLen) 67 | b[18] = t 68 | b = append(b, m...) 69 | return b 70 | } 71 | 72 | type AddPathTuple struct { 73 | AFI uint16 74 | SAFI uint8 75 | Tx bool 76 | Rx bool 77 | } 78 | 79 | func DecodeAddPathTuples(b []byte) ([]AddPathTuple, error) { 80 | if len(b) == 0 || len(b)%4 != 0 { 81 | return nil, &Notification{ 82 | Code: NOTIF_CODE_OPEN_MESSAGE_ERR, 83 | } 84 | } 85 | tuples := make([]AddPathTuple, 0, len(b)/4) 86 | for len(b) > 0 { 87 | var a AddPathTuple 88 | err := a.Decode(b) 89 | if err != nil { 90 | return nil, err 91 | } 92 | tuples = append(tuples, a) 93 | b = b[4:] 94 | } 95 | return tuples, nil 96 | } 97 | 98 | func (a *AddPathTuple) Decode(b []byte) error { 99 | if len(b) < 4 { 100 | return &Notification{ 101 | Code: NOTIF_CODE_OPEN_MESSAGE_ERR, 102 | } 103 | } 104 | a.AFI = binary.BigEndian.Uint16(b) 105 | a.SAFI = b[2] 106 | switch b[3] { 107 | case 3: 108 | a.Tx = true 109 | a.Rx = true 110 | case 2: 111 | a.Tx = true 112 | case 1: 113 | a.Rx = true 114 | default: 115 | return &Notification{ 116 | Code: NOTIF_CODE_OPEN_MESSAGE_ERR, 117 | } 118 | } 119 | return nil 120 | } 121 | 122 | func (a *AddPathTuple) Encode() []byte { 123 | b := make([]byte, 4) 124 | binary.BigEndian.PutUint16(b, a.AFI) 125 | b[2] = a.SAFI 126 | switch { 127 | // https://www.rfc-editor.org/rfc/rfc7911#page-4 128 | // Send/Receive: 129 | // This field indicates whether the sender is (a) able to receive 130 | // multiple paths from its peer (value 1), (b) able to send 131 | // multiple paths to its peer (value 2), or (c) both (value 3) for 132 | // the . 133 | case a.Tx && a.Rx: 134 | b[3] = 3 135 | case a.Tx: 136 | b[3] = 2 137 | case a.Rx: 138 | b[3] = 1 139 | } 140 | return b 141 | } 142 | 143 | // NewAddPathCapability returns an add-path Capability for the provided 144 | // AddPathTuples. 145 | func NewAddPathCapability(tuples []AddPathTuple) Capability { 146 | value := make([]byte, 0, 4*len(tuples)) 147 | for _, tuple := range tuples { 148 | value = append(value, tuple.Encode()...) 149 | } 150 | return Capability{ 151 | Code: CAP_ADD_PATH, 152 | Value: value, 153 | } 154 | } 155 | 156 | // NewMPExtensionsCapability returns a Multiprotocol Extensions Capability for 157 | // the provided AFI and SAFI. 158 | func NewMPExtensionsCapability(afi uint16, safi uint8) Capability { 159 | mpData := make([]byte, 4) 160 | binary.BigEndian.PutUint16(mpData, afi) 161 | mpData[3] = safi 162 | return Capability{ 163 | Code: CAP_MP_EXTENSIONS, 164 | Value: mpData, 165 | } 166 | } 167 | 168 | // Notification is a Notification message. 169 | type Notification struct { 170 | Code uint8 171 | Subcode uint8 172 | Data []byte 173 | } 174 | 175 | func newNotification(code, subcode uint8, data []byte) *Notification { 176 | return &Notification{ 177 | Code: code, 178 | Subcode: subcode, 179 | Data: data, 180 | } 181 | } 182 | 183 | func (n *Notification) messageType() uint8 { 184 | return notificationMessageType 185 | } 186 | 187 | func (n *Notification) decode(b []byte) error { 188 | /* 189 | If a peer sends a NOTIFICATION message, and the receiver of the 190 | message detects an error in that message, the receiver cannot use a 191 | NOTIFICATION message to report this error back to the peer. Any such 192 | error (e.g., an unrecognized Error Code or Error Subcode) SHOULD be 193 | noticed, logged locally, and brought to the attention of the 194 | administration of the peer. The means to do this, however, lies 195 | outside the scope of this document. 196 | */ 197 | if len(b) < 2 { 198 | return errors.New("notification message too short") 199 | } 200 | n.Code = b[0] 201 | n.Subcode = b[1] 202 | if len(b) > 2 { 203 | n.Data = make([]byte, len(b)-2) 204 | copy(n.Data, b[2:]) 205 | } 206 | return nil 207 | } 208 | 209 | func (n *Notification) encode() ([]byte, error) { 210 | b := make([]byte, 2) 211 | b[0] = n.Code 212 | b[1] = n.Subcode 213 | if len(n.Data) > 1 { 214 | b = append(b, n.Data...) 215 | } 216 | return prependHeader(b, notificationMessageType), nil 217 | } 218 | 219 | func (n *Notification) Error() string { 220 | var codeDesc, subcodeDesc string 221 | d, ok := notifCodesMap[n.Code] 222 | if ok { 223 | codeDesc = d.desc 224 | s, ok := d.subcodes[n.Subcode] 225 | if ok { 226 | subcodeDesc = s 227 | } 228 | } 229 | return fmt.Sprintf("notification code:%d (%s) subcode:%d (%s)", 230 | n.Code, codeDesc, n.Subcode, subcodeDesc) 231 | } 232 | 233 | func (n *Notification) AsSessionReset() *Notification { 234 | return n 235 | } 236 | 237 | type openMessage struct { 238 | version uint8 239 | asn uint16 240 | holdTime uint16 241 | bgpID uint32 242 | optionalParams []optionalParam 243 | } 244 | 245 | func (o *openMessage) messageType() uint8 { 246 | return openMessageType 247 | } 248 | 249 | // https://tools.ietf.org/html/rfc4271#section-6.2 250 | func (o *openMessage) validate(localID, localAS, remoteAS uint32) error { 251 | if o.version != 4 { 252 | version := make([]byte, 2) 253 | binary.BigEndian.PutUint16(version, uint16(4)) 254 | n := newNotification(NOTIF_CODE_OPEN_MESSAGE_ERR, 255 | NOTIF_SUBCODE_UNSUPPORTED_VERSION_NUM, version) 256 | return newNotificationError(n, true) 257 | } 258 | var fourOctetAS, fourOctetASFound bool 259 | if o.asn == asTrans { 260 | fourOctetAS = true 261 | } else if uint32(o.asn) != remoteAS { 262 | n := newNotification(NOTIF_CODE_OPEN_MESSAGE_ERR, 263 | NOTIF_SUBCODE_BAD_PEER_AS, nil) 264 | return newNotificationError(n, true) 265 | } 266 | if o.holdTime < 3 && o.holdTime != 0 { 267 | n := newNotification(NOTIF_CODE_OPEN_MESSAGE_ERR, 268 | NOTIF_SUBCODE_UNACCEPTABLE_HOLD_TIME, nil) 269 | return newNotificationError(n, true) 270 | } 271 | var id [4]byte 272 | binary.BigEndian.PutUint32(id[:], o.bgpID) 273 | addr := netip.AddrFrom4(id) 274 | if addr.IsMulticast() { 275 | n := newNotification(NOTIF_CODE_OPEN_MESSAGE_ERR, 276 | NOTIF_SUBCODE_BAD_BGP_ID, nil) 277 | return newNotificationError(n, true) 278 | } 279 | // https://tools.ietf.org/html/rfc6286#section-2.2 280 | if localAS == remoteAS && localID == o.bgpID { 281 | n := newNotification(NOTIF_CODE_OPEN_MESSAGE_ERR, 282 | NOTIF_SUBCODE_BAD_BGP_ID, nil) 283 | return newNotificationError(n, true) 284 | } 285 | caps := o.getCapabilities() 286 | for _, c := range caps { 287 | if c.Code == CAP_FOUR_OCTET_AS { 288 | fourOctetASFound = true 289 | if len(c.Value) != 4 { 290 | n := newNotification(NOTIF_CODE_OPEN_MESSAGE_ERR, 0, nil) 291 | return newNotificationError(n, true) 292 | } 293 | if binary.BigEndian.Uint32(c.Value) != remoteAS { 294 | n := newNotification(NOTIF_CODE_OPEN_MESSAGE_ERR, 295 | NOTIF_SUBCODE_BAD_PEER_AS, nil) 296 | return newNotificationError(n, true) 297 | } 298 | } 299 | } 300 | if fourOctetAS && !fourOctetASFound { 301 | n := newNotification(NOTIF_CODE_OPEN_MESSAGE_ERR, 302 | NOTIF_SUBCODE_BAD_PEER_AS, nil) 303 | return newNotificationError(n, true) 304 | } else if !fourOctetASFound { 305 | // corebgp requires four-octet ASN space support 306 | // 307 | // https://www.rfc-editor.org/rfc/rfc5492.html#section-5 308 | // This document defines a new Error Subcode, Unsupported Capability. 309 | // The value of this Subcode is 7. The Data field in the NOTIFICATION 310 | // message MUST list the set of capabilities that causes the speaker to 311 | // send the message. Each such capability is encoded in the same way as 312 | // it would be encoded in the OPEN message. 313 | // 314 | // As explained in the "Overview of Operations" section, the Unsupported 315 | // Capability NOTIFICATION is a way for a BGP speaker to complain that 316 | // its peer does not support a required capability without which the 317 | // peering cannot proceed. It MUST NOT be used when a BGP speaker 318 | // receives a capability that it does not understand; such capabilities 319 | // MUST be ignored. 320 | n := newNotification(NOTIF_CODE_OPEN_MESSAGE_ERR, 321 | NOTIF_SUBCODE_UNSUPPORTED_CAPABILITY, newFourOctetASCap(remoteAS).encode()) 322 | return newNotificationError(n, true) 323 | } 324 | return nil 325 | } 326 | 327 | func (o *openMessage) getCapabilities() []Capability { 328 | caps := make([]Capability, 0) 329 | for _, param := range o.optionalParams { 330 | p, isCap := param.(*capabilityOptionalParam) 331 | if isCap { 332 | caps = append(caps, p.capabilities...) 333 | } 334 | } 335 | return caps 336 | } 337 | 338 | func (o *openMessage) decode(b []byte) error { 339 | if len(b) < 10 { 340 | n := newNotification(NOTIF_CODE_MESSAGE_HEADER_ERR, 341 | NOTIF_SUBCODE_BAD_MESSAGE_LEN, b) 342 | return newNotificationError(n, true) 343 | } 344 | o.version = b[0] 345 | o.asn = binary.BigEndian.Uint16(b[1:3]) 346 | o.holdTime = binary.BigEndian.Uint16(b[3:5]) 347 | o.bgpID = binary.BigEndian.Uint32(b[5:9]) 348 | optionalParamsLen := int(b[9]) 349 | if optionalParamsLen != len(b)-10 { 350 | n := newNotification(NOTIF_CODE_OPEN_MESSAGE_ERR, 0, nil) 351 | return newNotificationError(n, true) 352 | } 353 | optionalParams, err := decodeOptionalParams(b[10:]) 354 | if err != nil { 355 | return err 356 | } 357 | o.optionalParams = optionalParams 358 | return nil 359 | } 360 | 361 | func decodeOptionalParams(b []byte) ([]optionalParam, error) { 362 | params := make([]optionalParam, 0) 363 | for { 364 | if len(b) < 2 { 365 | n := newNotification(NOTIF_CODE_OPEN_MESSAGE_ERR, 0, nil) 366 | return nil, newNotificationError(n, true) 367 | } 368 | paramCode := b[0] 369 | paramLen := b[1] 370 | if len(b) < int(paramLen)+2 { 371 | n := newNotification(NOTIF_CODE_OPEN_MESSAGE_ERR, 0, nil) 372 | return nil, newNotificationError(n, true) 373 | } 374 | paramToDecode := make([]byte, 0) 375 | if paramLen > 0 { 376 | paramToDecode = b[2 : paramLen+2] 377 | } 378 | nextParam := 2 + int(paramLen) 379 | b = b[nextParam:] 380 | switch paramCode { 381 | case capabilityOptionalParamType: 382 | c := &capabilityOptionalParam{} 383 | err := c.decode(paramToDecode) 384 | if err != nil { 385 | return nil, err 386 | } 387 | params = append(params, c) 388 | default: 389 | n := newNotification(NOTIF_CODE_OPEN_MESSAGE_ERR, 390 | NOTIF_SUBCODE_UNSUPPORTED_OPTIONAL_PARAM, nil) 391 | return nil, newNotificationError(n, true) 392 | } 393 | if len(b) == 0 { 394 | break 395 | } 396 | } 397 | return params, nil 398 | } 399 | 400 | func (o *openMessage) encode() ([]byte, error) { 401 | b := make([]byte, 9) 402 | b[0] = o.version 403 | binary.BigEndian.PutUint16(b[1:3], o.asn) 404 | binary.BigEndian.PutUint16(b[3:5], o.holdTime) 405 | binary.BigEndian.PutUint32(b[5:9], o.bgpID) 406 | params := make([]byte, 0) 407 | for _, param := range o.optionalParams { 408 | p, err := param.encode() 409 | if err != nil { 410 | return nil, err 411 | } 412 | params = append(params, p...) 413 | } 414 | b = append(b, uint8(len(params))) 415 | b = append(b, params...) 416 | return prependHeader(b, openMessageType), nil 417 | } 418 | 419 | const ( 420 | asTrans uint16 = 23456 421 | ) 422 | 423 | func newFourOctetASCap(asn uint32) Capability { 424 | c := Capability{ 425 | Code: CAP_FOUR_OCTET_AS, 426 | Value: make([]byte, 4), 427 | } 428 | binary.BigEndian.PutUint32(c.Value, asn) 429 | return c 430 | } 431 | 432 | func newOpenMessage(asn uint32, holdTime time.Duration, bgpID uint32, 433 | caps []Capability) (*openMessage, error) { 434 | allCaps := make([]Capability, 0, len(caps)+1) 435 | allCaps = append(allCaps, newFourOctetASCap(asn)) 436 | for _, c := range caps { 437 | // ignore four octet as capability as we include this implicitly above 438 | if c.Code != CAP_FOUR_OCTET_AS { 439 | allCaps = append(allCaps, c) 440 | } 441 | } 442 | o := &openMessage{ 443 | version: 4, 444 | holdTime: uint16(holdTime.Truncate(time.Second).Seconds()), 445 | bgpID: bgpID, 446 | optionalParams: []optionalParam{ 447 | &capabilityOptionalParam{ 448 | capabilities: allCaps, 449 | }, 450 | }, 451 | } 452 | if asn > math.MaxUint16 { 453 | o.asn = asTrans 454 | } else { 455 | o.asn = uint16(asn) 456 | } 457 | return o, nil 458 | } 459 | 460 | const ( 461 | capabilityOptionalParamType uint8 = 2 462 | ) 463 | 464 | type optionalParam interface { 465 | paramType() uint8 466 | encode() ([]byte, error) 467 | decode(b []byte) error 468 | } 469 | 470 | type capabilityOptionalParam struct { 471 | capabilities []Capability 472 | } 473 | 474 | func (c *capabilityOptionalParam) paramType() uint8 { 475 | return capabilityOptionalParamType 476 | } 477 | 478 | func (c *capabilityOptionalParam) decode(b []byte) error { 479 | for { 480 | if len(b) < 2 { 481 | n := newNotification(NOTIF_CODE_OPEN_MESSAGE_ERR, 0, nil) 482 | return newNotificationError(n, true) 483 | } 484 | capCode := b[0] 485 | capLen := b[1] 486 | if len(b) < int(capLen)+2 { 487 | n := newNotification(NOTIF_CODE_OPEN_MESSAGE_ERR, 0, nil) 488 | return newNotificationError(n, true) 489 | } 490 | capValue := make([]byte, 0) 491 | if capLen > 0 { 492 | capValue = b[2 : capLen+2] 493 | } 494 | capability := Capability{ 495 | Code: capCode, 496 | Value: capValue, 497 | } 498 | c.capabilities = append(c.capabilities, capability) 499 | nextCap := 2 + int(capLen) 500 | b = b[nextCap:] 501 | if len(b) == 0 { 502 | return nil 503 | } 504 | } 505 | } 506 | 507 | func (c *capabilityOptionalParam) encode() ([]byte, error) { 508 | b := make([]byte, 0) 509 | caps := make([]byte, 0) 510 | if len(c.capabilities) > 0 { 511 | for _, capability := range c.capabilities { 512 | caps = append(caps, capability.encode()...) 513 | } 514 | } else { 515 | return nil, errors.New("empty capabilities in capability optional param") 516 | } 517 | b = append(b, capabilityOptionalParamType) 518 | b = append(b, uint8(len(caps))) 519 | b = append(b, caps...) 520 | return b, nil 521 | } 522 | 523 | // Capability is a BGP capability as defined by RFC5492. 524 | type Capability struct { 525 | Code uint8 526 | Value []byte 527 | } 528 | 529 | func (c Capability) Equal(d Capability) bool { 530 | if c.Code != d.Code { 531 | return false 532 | } 533 | return bytes.Equal(c.Value, d.Value) 534 | } 535 | 536 | func (c Capability) encode() []byte { 537 | b := make([]byte, 2+len(c.Value)) 538 | b[0] = c.Code 539 | b[1] = uint8(len(c.Value)) 540 | copy(b[2:], c.Value) 541 | return b 542 | } 543 | 544 | type updateMessage []byte 545 | 546 | func (u updateMessage) messageType() uint8 { 547 | return updateMessageType 548 | } 549 | 550 | type keepAliveMessage struct{} 551 | 552 | func (k keepAliveMessage) messageType() uint8 { 553 | return keepAliveMessageType 554 | } 555 | 556 | func (k keepAliveMessage) encode() ([]byte, error) { 557 | return prependHeader(nil, keepAliveMessageType), nil 558 | } 559 | -------------------------------------------------------------------------------- /packet_test.go: -------------------------------------------------------------------------------- 1 | package corebgp 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCapability_Equal(t *testing.T) { 11 | type fields struct { 12 | Code uint8 13 | Value []byte 14 | } 15 | type args struct { 16 | d Capability 17 | } 18 | tests := []struct { 19 | name string 20 | fields fields 21 | args args 22 | want bool 23 | }{ 24 | { 25 | name: "equal", 26 | fields: fields{ 27 | Code: 1, 28 | Value: []byte{1}, 29 | }, 30 | args: args{ 31 | d: Capability{ 32 | Code: 1, 33 | Value: []byte{1}, 34 | }, 35 | }, 36 | want: true, 37 | }, 38 | { 39 | name: "unequal code", 40 | fields: fields{ 41 | Code: 1, 42 | Value: []byte{1}, 43 | }, 44 | args: args{ 45 | d: Capability{ 46 | Code: 2, 47 | Value: []byte{1}, 48 | }, 49 | }, 50 | want: false, 51 | }, 52 | { 53 | name: "unequal value", 54 | fields: fields{ 55 | Code: 1, 56 | Value: []byte{1}, 57 | }, 58 | args: args{ 59 | d: Capability{ 60 | Code: 1, 61 | Value: []byte{2}, 62 | }, 63 | }, 64 | want: false, 65 | }, 66 | { 67 | name: "equal nil and empty value", 68 | fields: fields{ 69 | Code: 1, 70 | Value: []byte{}, 71 | }, 72 | args: args{ 73 | d: Capability{ 74 | Code: 1, 75 | Value: nil, 76 | }, 77 | }, 78 | want: true, 79 | }, 80 | } 81 | for _, tt := range tests { 82 | t.Run(tt.name, func(t *testing.T) { 83 | c := Capability{ 84 | Code: tt.fields.Code, 85 | Value: tt.fields.Value, 86 | } 87 | assert.Equalf(t, tt.want, c.Equal(tt.args.d), "Equal(%v)", tt.args.d) 88 | }) 89 | } 90 | } 91 | 92 | func TestDecodeAddPathTuples(t *testing.T) { 93 | type args struct { 94 | b []byte 95 | } 96 | tests := []struct { 97 | name string 98 | args args 99 | want []AddPathTuple 100 | wantErr assert.ErrorAssertionFunc 101 | }{ 102 | { 103 | name: "valid tuples", 104 | args: args{ 105 | b: []byte{ 106 | 0x00, 0x01, // afi 107 | 0x01, // safi 108 | 0x02, // tx 109 | 0x00, 0x02, // afi 110 | 0x01, // safi 111 | 0x01, // tx 112 | 0x00, 0x03, // afi 113 | 0x01, // safi 114 | 0x03, // tx 115 | }, 116 | }, 117 | want: []AddPathTuple{ 118 | { 119 | AFI: 1, 120 | SAFI: 1, 121 | Tx: true, 122 | }, 123 | { 124 | AFI: 2, 125 | SAFI: 1, 126 | Rx: true, 127 | }, 128 | { 129 | AFI: 3, 130 | SAFI: 1, 131 | Tx: true, 132 | Rx: true, 133 | }, 134 | }, 135 | wantErr: assert.NoError, 136 | }, 137 | { 138 | name: "invalid tuple on tail", 139 | args: args{ 140 | b: []byte{ 141 | 0x00, 0x01, // afi 142 | 0x01, // safi 143 | 0x02, // tx 144 | 0x00, 0x02, // afi 145 | 0x01, // safi 146 | 0x01, // tx 147 | 0x00, 0x03, // afi 148 | 0x01, // safi 149 | 0x03, // tx 150 | 0x00, 0x03, // afi 151 | 0x01, // safi 152 | // no direction octet 153 | }, 154 | }, 155 | want: nil, 156 | wantErr: assert.Error, 157 | }, 158 | } 159 | for _, tt := range tests { 160 | t.Run(tt.name, func(t *testing.T) { 161 | got, err := DecodeAddPathTuples(tt.args.b) 162 | if !tt.wantErr(t, err, fmt.Sprintf("DecodeAddPathTuples(%v)", tt.args.b)) { 163 | return 164 | } 165 | assert.Equalf(t, tt.want, got, "DecodeAddPathTuples(%v)", tt.args.b) 166 | }) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /peer.go: -------------------------------------------------------------------------------- 1 | package corebgp 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | const ( 11 | // the amount of time after which we forget about a previously encountered 12 | // protocol error leading to a reset of startupDelay 13 | errorAmnesiaTime = time.Second * 300 14 | // the minimum amount of startup delay incurred from a protocol error 15 | errorDelayMinTime = time.Second * 60 16 | // the maximum amount of startup delay incurred from a protocol error 17 | errorDelayMaxTime = time.Second * 300 18 | ) 19 | 20 | // peer manages the FSMs for a peer. 21 | type peer struct { 22 | config PeerConfig 23 | id uint32 24 | plugin Plugin 25 | options peerOptions 26 | 27 | fsms [2]*fsm 28 | fsmState [2]fsmState 29 | transitionCh [2]chan stateTransition 30 | errorCh [2]chan error 31 | 32 | lastProtoError *time.Time 33 | startupDelay time.Duration 34 | startupDelayTimer *time.Timer 35 | inHoldDown bool 36 | 37 | inConnCh chan net.Conn 38 | closeOnce sync.Once 39 | closeCh chan struct{} 40 | doneCh chan struct{} 41 | } 42 | 43 | const ( 44 | out = 0 45 | in = 1 46 | ) 47 | 48 | func newPeer(config PeerConfig, id uint32, plugin Plugin, options peerOptions) *peer { 49 | p := &peer{ 50 | config: config, 51 | id: id, 52 | plugin: plugin, 53 | options: options, 54 | inConnCh: make(chan net.Conn), 55 | closeCh: make(chan struct{}), 56 | doneCh: make(chan struct{}), 57 | startupDelayTimer: time.NewTimer(0), 58 | } 59 | <-p.startupDelayTimer.C 60 | for i := 0; i < 2; i++ { 61 | p.fsmState[i] = disabledState 62 | p.transitionCh[i] = make(chan stateTransition) 63 | p.errorCh[i] = make(chan error) 64 | } 65 | return p 66 | } 67 | 68 | // getFSMTransitionCh returns the stateTransition channel for the provided FSM. 69 | func (p *peer) getFSMTransitionCh(f *fsm) chan stateTransition { 70 | if f == p.fsms[out] { 71 | return p.transitionCh[out] 72 | } 73 | return p.transitionCh[in] 74 | } 75 | 76 | // getFSMErrorCh returns the error channel for the provided FSM. 77 | func (p *peer) getFSMErrorCh(f *fsm) chan error { 78 | if f == p.fsms[out] { 79 | return p.errorCh[out] 80 | } 81 | return p.errorCh[in] 82 | } 83 | 84 | func other(i int) int { 85 | if i == out { 86 | return in 87 | } 88 | return out 89 | } 90 | 91 | func (p *peer) logTransition(i int, from, to fsmState) { 92 | logf("[%s] FSM-%s transition %s => %s", p.config.RemoteAddress, 93 | direction(i), from, to) 94 | } 95 | 96 | func (p *peer) disableFSM(i int) { 97 | if p.fsms[i] == nil { 98 | return 99 | } 100 | p.logTransition(i, p.fsmState[i], disabledState) 101 | p.fsms[i].stop() 102 | p.fsms[i] = nil 103 | p.fsmState[i] = disabledState 104 | } 105 | 106 | func (p *peer) sendTransitionToFSM(i int, t stateTransition) { 107 | select { 108 | case <-p.closeCh: 109 | return 110 | case p.transitionCh[i] <- t: 111 | p.logTransition(i, t.from, t.to) 112 | p.fsmState[i] = t.to 113 | } 114 | } 115 | 116 | func (p *peer) enableFSM(i int, conn net.Conn) { 117 | if i == out && p.options.passive { 118 | return 119 | } 120 | if p.fsms[i] == nil { 121 | p.fsms[i] = newFSM(p, conn) 122 | p.fsmState[i] = disabledState 123 | p.fsms[i].start() 124 | } 125 | } 126 | 127 | func (p *peer) handleStateTransition(i int, t stateTransition) { 128 | switch { 129 | case t.to == establishedState: 130 | // disable the other fsm 131 | p.disableFSM(other(i)) 132 | p.sendTransitionToFSM(i, t) 133 | case i == in && t.to < t.from: 134 | // in going down, disable it and make sure out is enabled 135 | p.disableFSM(i) 136 | p.enableFSM(out, nil) 137 | case t.to == openConfirmState: 138 | // https://tools.ietf.org/html/rfc4271#section-6.8 139 | switch p.fsmState[other(i)] { 140 | case establishedState: 141 | /* 142 | Unless allowed via configuration, a connection collision with an 143 | existing BGP connection that is in the Established state causes 144 | closing of the newly created connection. 145 | */ 146 | p.disableFSM(i) 147 | case openConfirmState: 148 | // https://github.com/BIRD/bird/blob/v2.0.2/proto/bgp/packets.c#L666 149 | /* 150 | Description of collision detection rules in RFC 4271 is confusing and 151 | contradictory, but it is essentially: 152 | 153 | 1. Router with higher ID is dominant 154 | 2. If both have the same ID, router with higher ASN is dominant [RFC6286] 155 | 3. When both connections are in OpenConfirm state, one initiated by 156 | the dominant router is kept. 157 | */ 158 | remoteID := p.fsms[i].remoteID 159 | localID := p.id 160 | dominant := localID > remoteID || 161 | (localID == remoteID) && (p.config.LocalAS > p.config.RemoteAS) 162 | if dominant && i == out { 163 | // attempt to disable other FSM 164 | select { 165 | case <-p.closeCh: 166 | return 167 | case p.fsms[other(i)].closeCh <- struct{}{}: 168 | // we send an empty struct rather than close the channel in 169 | // case we lose on the select race in fsm.openConfirm() 170 | p.disableFSM(other(i)) // wait for it to stop completely 171 | p.sendTransitionToFSM(i, t) 172 | case otherT := <-p.transitionCh[other(i)]: 173 | // other FSM transitioned before we could disable it 174 | if otherT.to == establishedState { 175 | // other FSM entered established state before we could 176 | // disable it. disable this FSM and then handle the 177 | // transition from the other FSM. 178 | p.disableFSM(i) 179 | p.handleStateTransition(other(i), otherT) 180 | } else { 181 | // other FSM went down, allow this FSM to transition to 182 | // openConfirm and then handle the transition from the 183 | // other FSM. 184 | p.sendTransitionToFSM(i, t) 185 | p.handleStateTransition(other(i), otherT) 186 | } 187 | } 188 | } else { 189 | // disable this fsm 190 | p.disableFSM(i) 191 | } 192 | default: 193 | p.sendTransitionToFSM(i, t) 194 | } 195 | default: 196 | p.sendTransitionToFSM(i, t) 197 | } 198 | } 199 | 200 | func direction(i int) string { 201 | if i == in { 202 | return "in" 203 | } 204 | return "out" 205 | } 206 | 207 | // handleError handles an error during fsm operation 208 | func (p *peer) handleError(i int, err error) { 209 | logf("[%s] FSM-%s %s error: %v", 210 | p.config.RemoteAddress, direction(i), p.fsmState[i], err) 211 | var nerr *notificationError 212 | if errors.As(err, &nerr) { 213 | if nerr.dampPeer() { 214 | p.disableFSM(in) 215 | p.disableFSM(out) 216 | p.updateStartupDelay() 217 | p.inHoldDown = true 218 | } 219 | } 220 | } 221 | 222 | // updateStartupDelay manages startupDelay and startupDelayTimer when an error 223 | // requiring damping occurs in one of the FSMs. This logic is strongly 224 | // influenced by bird's implementation found here 225 | // https://github.com/BIRD/bird/blob/v2.0.2/proto/bgp/bgp.c#L384 226 | func (p *peer) updateStartupDelay() { 227 | if p.lastProtoError != nil && 228 | (time.Since(*p.lastProtoError) >= errorAmnesiaTime) { 229 | p.startupDelay = 0 230 | } 231 | 232 | lastProtoError := time.Now() 233 | p.lastProtoError = &lastProtoError 234 | 235 | if p.startupDelay > 0 { 236 | p.startupDelay = min(2*p.startupDelay, errorDelayMaxTime) 237 | } else { 238 | p.startupDelay = errorDelayMinTime 239 | } 240 | 241 | p.startupDelayTimer.Stop() 242 | p.startupDelayTimer = time.NewTimer(p.startupDelay) 243 | logf("[%s] damping peer for %s", p.config.RemoteAddress, p.startupDelay) 244 | } 245 | 246 | // main run loop 247 | func (p *peer) run() { 248 | defer func() { 249 | p.disableFSM(out) 250 | p.disableFSM(in) 251 | p.startupDelayTimer.Stop() 252 | close(p.doneCh) 253 | }() 254 | 255 | for { 256 | select { 257 | case <-p.closeCh: 258 | return 259 | case <-p.startupDelayTimer.C: 260 | logf("[%s] startup delay timer expired, enabling peer", 261 | p.config.RemoteAddress) 262 | p.enableFSM(out, nil) 263 | p.inHoldDown = false 264 | case err := <-p.errorCh[in]: 265 | p.handleError(in, err) 266 | case err := <-p.errorCh[out]: 267 | p.handleError(out, err) 268 | case t := <-p.transitionCh[in]: 269 | p.handleStateTransition(in, t) 270 | case t := <-p.transitionCh[out]: 271 | p.handleStateTransition(out, t) 272 | case conn := <-p.inConnCh: 273 | if p.inHoldDown { 274 | conn.Close() 275 | continue 276 | } 277 | 278 | // https://github.com/BIRD/bird/blob/v2.0.2/proto/bgp/bgp.c#L1036 279 | if p.fsms[in] != nil || p.fsmState[out] == establishedState { 280 | conn.Close() 281 | continue 282 | } else { 283 | p.enableFSM(in, conn) 284 | } 285 | } 286 | } 287 | } 288 | 289 | func (p *peer) start() { 290 | p.enableFSM(out, nil) 291 | go p.run() 292 | } 293 | 294 | func (p *peer) stop() { 295 | p.closeOnce.Do(func() { 296 | close(p.closeCh) 297 | }) 298 | <-p.doneCh 299 | } 300 | 301 | func (p *peer) incomingConnection(conn net.Conn) { 302 | select { 303 | case <-p.closeCh: 304 | conn.Close() 305 | return 306 | case p.inConnCh <- conn: 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /peer_options.go: -------------------------------------------------------------------------------- 1 | package corebgp 2 | 3 | import ( 4 | "errors" 5 | "net/netip" 6 | "syscall" 7 | "time" 8 | ) 9 | 10 | type peerOptions struct { 11 | holdTime time.Duration 12 | idleHoldTime time.Duration 13 | connectRetryTime time.Duration 14 | port int 15 | passive bool 16 | dialerControlFn func(network, address string, c syscall.RawConn) error 17 | localAddress netip.Addr 18 | } 19 | 20 | func (p peerOptions) validate() error { 21 | if p.holdTime < time.Second*3 && p.holdTime != 0 { 22 | return errors.New("hold time must be 0 or >= 3 seconds") 23 | } 24 | if p.port < 1 || p.port > 65535 { 25 | return errors.New("port must be between 1 and 65535") 26 | } 27 | return nil 28 | } 29 | 30 | type PeerOption interface { 31 | apply(*peerOptions) 32 | } 33 | 34 | const ( 35 | // DefaultHoldTimeSeconds is the default hold down time in seconds. 36 | DefaultHoldTimeSeconds uint16 = 90 37 | // DefaultIdleHoldTime is the default idle state hold time for a peer. 38 | DefaultIdleHoldTime = time.Second * 5 39 | // DefaultConnectRetryTime is the default maximum time spent waiting for an 40 | // outbound dial to connect. 41 | // 42 | // https://tools.ietf.org/html/rfc4271#section-8.2.2 43 | // The exact value of the ConnectRetryTimer is a local matter, but it 44 | // SHOULD be sufficiently large to allow TCP initialization. 45 | DefaultConnectRetryTime = time.Second * 5 46 | // DefaultPort is the default TCP port for a peer. 47 | DefaultPort = 179 48 | ) 49 | 50 | func defaultPeerOptions() peerOptions { 51 | return peerOptions{ 52 | holdTime: time.Second * time.Duration(DefaultHoldTimeSeconds), 53 | idleHoldTime: DefaultIdleHoldTime, 54 | connectRetryTime: DefaultConnectRetryTime, 55 | port: DefaultPort, 56 | passive: false, 57 | localAddress: netip.Addr{}, 58 | } 59 | } 60 | 61 | type funcPeerOption struct { 62 | fn func(*peerOptions) 63 | } 64 | 65 | func (f *funcPeerOption) apply(p *peerOptions) { 66 | f.fn(p) 67 | } 68 | 69 | func newFuncPeerOption(f func(*peerOptions)) *funcPeerOption { 70 | return &funcPeerOption{ 71 | fn: f, 72 | } 73 | } 74 | 75 | // WithPassive returns a PeerOption that sets a Peer to passive mode. In passive 76 | // mode a peer will not dial out and will only accept incoming connections. 77 | func WithPassive() PeerOption { 78 | return newFuncPeerOption(func(o *peerOptions) { 79 | o.passive = true 80 | }) 81 | } 82 | 83 | // WithIdleHoldTime returns a PeerOption that sets the idle hold time for a 84 | // peer. Idle hold time controls how quickly a peer can oscillate from idle to 85 | // the connect state. 86 | func WithIdleHoldTime(t time.Duration) PeerOption { 87 | return newFuncPeerOption(func(o *peerOptions) { 88 | o.idleHoldTime = t 89 | }) 90 | } 91 | 92 | // WithConnectRetryTime returns a PeerOption that sets the connect retry time 93 | // for a peer. 94 | func WithConnectRetryTime(t time.Duration) PeerOption { 95 | return newFuncPeerOption(func(o *peerOptions) { 96 | o.connectRetryTime = t 97 | }) 98 | } 99 | 100 | // WithPort returns a PeerOption that sets the TCP port for a peer. 101 | func WithPort(p int) PeerOption { 102 | return newFuncPeerOption(func(o *peerOptions) { 103 | o.port = p 104 | }) 105 | } 106 | 107 | // WithDialerControl returns a PeerOption that sets the outbound net.Dialer 108 | // Control field. This is commonly used to set socket options, e.g. ip TTL, tcp 109 | // md5, tcp_nodelay, etc... 110 | func WithDialerControl(fn func(network, address string, 111 | c syscall.RawConn) error) PeerOption { 112 | return newFuncPeerOption(func(o *peerOptions) { 113 | o.dialerControlFn = fn 114 | }) 115 | } 116 | 117 | // WithLocalAddress returns a PeerOption that specifies the source address to 118 | // use when dialing outbound, and to verify as a destination for inbound 119 | // connections. Without this PeerOption corebgp behaves loosely, accepting 120 | // inbound connections regardless of the destination address, and falling back 121 | // on the OS for outbound source address selection. 122 | func WithLocalAddress(localAddress netip.Addr) PeerOption { 123 | return newFuncPeerOption(func(o *peerOptions) { 124 | o.localAddress = localAddress 125 | }) 126 | } 127 | 128 | // WithHoldTime returns a PeerOption that sets the hold time (in seconds) to be 129 | // advertised to the peer via OPEN message. Hold time MUST be 0 or >= 3 seconds. 130 | func WithHoldTime(seconds uint16) PeerOption { 131 | return newFuncPeerOption(func(o *peerOptions) { 132 | o.holdTime = time.Duration(seconds) * time.Second 133 | }) 134 | } 135 | -------------------------------------------------------------------------------- /plugin.go: -------------------------------------------------------------------------------- 1 | package corebgp 2 | 3 | import "net/netip" 4 | 5 | // Plugin is a BGP peer plugin. 6 | type Plugin interface { 7 | // GetCapabilities is fired when a peer's FSM is in the Connect state prior 8 | // to sending an Open message. The returned capabilities are included in the 9 | // Open message sent to the peer. 10 | // 11 | // The four-octet AS number space capability will be implicitly handled, 12 | // Plugin implementations are not required to return it. 13 | GetCapabilities(peer PeerConfig) []Capability 14 | 15 | // OnOpenMessage is fired when an Open message is received from a peer 16 | // during the OpenSent state. Returning a non-nil Notification will cause it 17 | // to be sent to the peer and the FSM will transition to the Idle state. 18 | // 19 | // Remote peers MUST include the four-octet AS number space capability in 20 | // their open message. corebgp will return a Notification message if a 21 | // remote peer does not support said capability, and will not invoke 22 | // OnOpenMessage. 23 | // 24 | // Per RFC5492 a BGP speaker should only send a Notification if a required 25 | // capability is missing; unknown or unsupported capabilities should be 26 | // ignored. 27 | OnOpenMessage(peer PeerConfig, routerID netip.Addr, capabilities []Capability) *Notification 28 | 29 | // OnEstablished is fired when a peer's FSM transitions to the Established 30 | // state. The returned UpdateMessageHandler will be fired when an Update 31 | // message is received from the peer. 32 | // 33 | // The provided writer can be used to send Update messages to the peer for 34 | // the lifetime of the FSM's current, established state. It should be 35 | // discarded once OnClose() fires. 36 | OnEstablished(peer PeerConfig, writer UpdateMessageWriter) UpdateMessageHandler 37 | 38 | // OnClose is fired when a peer's FSM transitions out of the Established 39 | // state. 40 | OnClose(peer PeerConfig) 41 | } 42 | 43 | // UpdateMessageHandler handles Update messages. If a non-nil Notification is 44 | // returned it will be sent to the peer and the FSM will transition out of the 45 | // Established state. 46 | type UpdateMessageHandler func(peer PeerConfig, updateMessage []byte) *Notification 47 | 48 | type UpdateMessageWriter interface { 49 | // WriteUpdate sends an update message to the remote peer. An error is 50 | // returned if the write fails and/or the FSM is no longer in an established 51 | // state. 52 | WriteUpdate([]byte) error 53 | } 54 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package corebgp 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "net/netip" 9 | "sync" 10 | ) 11 | 12 | // Server is a BGP server that manages peers. 13 | type Server struct { 14 | mu sync.Mutex 15 | id uint32 16 | peers map[string]*peer 17 | 18 | // control channels & run state 19 | serving bool 20 | doneServingCh chan struct{} 21 | closeCh chan struct{} 22 | closeOnce sync.Once 23 | } 24 | 25 | // NewServer creates a new Server. 26 | func NewServer(routerID netip.Addr) (*Server, error) { 27 | if !routerID.Is4() { 28 | return nil, errors.New("invalid router ID") 29 | } 30 | 31 | s := &Server{ 32 | mu: sync.Mutex{}, 33 | id: binary.BigEndian.Uint32(routerID.AsSlice()), 34 | peers: make(map[string]*peer), 35 | doneServingCh: make(chan struct{}), 36 | closeCh: make(chan struct{}), 37 | } 38 | return s, nil 39 | } 40 | 41 | var ( 42 | ErrServerClosed = errors.New("server closed") 43 | ErrPeerNotExist = errors.New("peer does not exist") 44 | ErrPeerAlreadyExists = errors.New("peer already exists") 45 | ) 46 | 47 | func (s *Server) handleInboundConn(conn net.Conn) { 48 | h, _, err := net.SplitHostPort(conn.RemoteAddr().String()) 49 | if err != nil { 50 | conn.Close() 51 | return 52 | } 53 | s.mu.Lock() 54 | defer s.mu.Unlock() 55 | p, exists := s.peers[h] 56 | if !exists { 57 | conn.Close() 58 | return 59 | } 60 | if p.options.localAddress.IsValid() { 61 | h, _, err = net.SplitHostPort(conn.LocalAddr().String()) 62 | laddr, _ := netip.ParseAddr(h) 63 | if err != nil || p.options.localAddress != laddr { 64 | conn.Close() 65 | return 66 | } 67 | } 68 | p.incomingConnection(conn) 69 | } 70 | 71 | // Serve starts all peers' FSMs, starts handling incoming connections if a 72 | // non-nil listener is provided, and then blocks. Serve returns ErrServerClosed 73 | // upon Close() or a listener error if one occurs. 74 | func (s *Server) Serve(listeners []net.Listener) error { 75 | s.mu.Lock() 76 | // check if server has been closed 77 | select { 78 | case <-s.doneServingCh: 79 | s.mu.Unlock() 80 | return ErrServerClosed 81 | case <-s.closeCh: 82 | s.mu.Unlock() 83 | return ErrServerClosed 84 | default: 85 | } 86 | 87 | // set serving state and enable peers 88 | s.serving = true 89 | for _, peer := range s.peers { 90 | peer.start() 91 | } 92 | s.mu.Unlock() 93 | 94 | defer func() { 95 | // disable peers and set serving state before returning 96 | s.mu.Lock() 97 | for _, peer := range s.peers { 98 | peer.stop() 99 | } 100 | s.serving = false 101 | close(s.doneServingCh) 102 | s.mu.Unlock() 103 | }() 104 | 105 | lisErrCh := make(chan error) 106 | lisWG := &sync.WaitGroup{} 107 | closingListeners := make(chan struct{}) 108 | for _, lis := range listeners { 109 | lisWG.Add(1) 110 | go func(lis net.Listener) { 111 | defer lisWG.Done() 112 | for { 113 | conn, err := lis.Accept() 114 | if err != nil { 115 | select { 116 | case lisErrCh <- err: 117 | case <-closingListeners: 118 | } 119 | return 120 | } 121 | s.handleInboundConn(conn) 122 | } 123 | }(lis) 124 | } 125 | 126 | closeListeners := func() { 127 | close(closingListeners) 128 | for _, lis := range listeners { 129 | lis.Close() 130 | } 131 | lisWG.Wait() 132 | } 133 | 134 | select { 135 | case <-s.closeCh: 136 | closeListeners() 137 | return ErrServerClosed 138 | case err := <-lisErrCh: 139 | closeListeners() 140 | return fmt.Errorf("listener error: %v", err) 141 | } 142 | } 143 | 144 | // Close stops the Server. An instance of a stopped Server cannot be re-used. 145 | func (s *Server) Close() { 146 | s.mu.Lock() 147 | s.closeOnce.Do(func() { 148 | close(s.closeCh) 149 | }) 150 | if !s.serving { 151 | s.mu.Unlock() 152 | return 153 | } 154 | s.mu.Unlock() 155 | <-s.doneServingCh 156 | } 157 | 158 | // PeerConfig is the required configuration for a Peer. 159 | type PeerConfig struct { 160 | // RemoteAddress is the remote address of the peer. 161 | RemoteAddress netip.Addr 162 | 163 | // LocalAS is the local autonomous system number to populate in outbound 164 | // OPEN messages. 165 | LocalAS uint32 166 | 167 | // RemoteAS is the autonomous system number to expect in OPEN messages 168 | // from this peer. 169 | RemoteAS uint32 170 | } 171 | 172 | func (p PeerConfig) validate(opts peerOptions) error { 173 | if !opts.localAddress.IsValid() && p.RemoteAddress.IsValid() { 174 | return nil 175 | } 176 | localIsIPv4 := opts.localAddress.Is4() 177 | remoteIsIPv4 := p.RemoteAddress.Is4() 178 | if localIsIPv4 != remoteIsIPv4 { 179 | return errors.New("mixed address family peer address pair") 180 | } 181 | if !localIsIPv4 { 182 | if !opts.localAddress.Is6() || !p.RemoteAddress.Is6() { 183 | return errors.New("invalid peer address pair") 184 | } 185 | } 186 | // https://tools.ietf.org/html/rfc7607 187 | // 188 | // If a BGP speaker receives zero as the peer AS in an OPEN message, it 189 | // MUST abort the connection and send a NOTIFICATION with Error Code 190 | // "OPEN Message Error" and subcode "Bad Peer AS" (see Section 6 of 191 | // [RFC4271]). A router MUST NOT initiate a connection claiming to be 192 | // AS 0. 193 | if p.LocalAS == 0 || p.RemoteAS == 0 { 194 | return errors.New("AS must be > 0") 195 | } 196 | return nil 197 | } 198 | 199 | // AddPeer adds a peer to the Server to be handled with the provided Plugin and 200 | // PeerOptions. 201 | func (s *Server) AddPeer(config PeerConfig, plugin Plugin, 202 | opts ...PeerOption) error { 203 | o := defaultPeerOptions() 204 | for _, opt := range opts { 205 | opt.apply(&o) 206 | } 207 | err := o.validate() 208 | if err != nil { 209 | return fmt.Errorf("invalid peer options: %v", err) 210 | } 211 | err = config.validate(o) 212 | if err != nil { 213 | return fmt.Errorf("peer config invalid: %v", err) 214 | } 215 | s.mu.Lock() 216 | defer s.mu.Unlock() 217 | _, exists := s.peers[config.RemoteAddress.String()] 218 | if exists { 219 | return ErrPeerAlreadyExists 220 | } 221 | p := newPeer(config, s.id, plugin, o) 222 | if s.serving { 223 | p.start() 224 | } 225 | s.peers[p.config.RemoteAddress.String()] = p 226 | return nil 227 | } 228 | 229 | // DeletePeer deletes a peer from the Server. 230 | func (s *Server) DeletePeer(ip netip.Addr) error { 231 | s.mu.Lock() 232 | defer s.mu.Unlock() 233 | p, exists := s.peers[ip.String()] 234 | if !exists { 235 | return ErrPeerNotExist 236 | } 237 | if s.serving { 238 | p.stop() 239 | } 240 | delete(s.peers, ip.String()) 241 | return nil 242 | } 243 | 244 | // GetPeer returns the configuration for the provided peer, or an error if it 245 | // does not exist. 246 | func (s *Server) GetPeer(ip netip.Addr) (PeerConfig, error) { 247 | s.mu.Lock() 248 | defer s.mu.Unlock() 249 | p, exists := s.peers[ip.String()] 250 | if !exists { 251 | return PeerConfig{}, ErrPeerNotExist 252 | } 253 | return p.config, nil 254 | } 255 | 256 | // ListPeers returns the configuration for all peers. 257 | func (s *Server) ListPeers() []PeerConfig { 258 | s.mu.Lock() 259 | defer s.mu.Unlock() 260 | configs := make([]PeerConfig, 0) 261 | for _, peer := range s.peers { 262 | configs = append(configs, peer.config) 263 | } 264 | return configs 265 | } 266 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package corebgp 2 | 3 | import ( 4 | "net/netip" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestServer(t *testing.T) { 12 | _, err := NewServer(netip.Addr{}) 13 | assert.Error(t, err) 14 | 15 | _, err = NewServer(netip.MustParseAddr("::1")) 16 | assert.Error(t, err) 17 | 18 | s, err := NewServer(netip.MustParseAddr("127.0.0.1")) 19 | assert.NoError(t, err) 20 | 21 | err = s.AddPeer(PeerConfig{}, nil) 22 | assert.Error(t, err) 23 | 24 | err = s.AddPeer(PeerConfig{ 25 | RemoteAddress: netip.MustParseAddr("127.0.0.2"), 26 | LocalAS: 64512, 27 | RemoteAS: 64513, 28 | }, nil, WithLocalAddress(netip.MustParseAddr("::1"))) 29 | assert.Error(t, err) 30 | 31 | pcIPv4 := PeerConfig{ 32 | RemoteAddress: netip.MustParseAddr("127.0.0.2"), 33 | LocalAS: 64512, 34 | RemoteAS: 64513, 35 | } 36 | err = s.AddPeer(pcIPv4, nil, WithLocalAddress(netip.MustParseAddr("127.0.0.1"))) 37 | assert.NoError(t, err) 38 | err = s.AddPeer(pcIPv4, nil, WithLocalAddress(netip.MustParseAddr("127.0.0.1"))) 39 | assert.ErrorIs(t, err, ErrPeerAlreadyExists) 40 | 41 | pcIPv6 := PeerConfig{ 42 | RemoteAddress: netip.MustParseAddr("::2"), 43 | LocalAS: 64512, 44 | RemoteAS: 64513, 45 | } 46 | err = s.AddPeer(pcIPv6, nil, WithLocalAddress(netip.MustParseAddr("::1"))) 47 | assert.NoError(t, err) 48 | err = s.AddPeer(pcIPv6, nil, WithLocalAddress(netip.MustParseAddr("::1"))) 49 | assert.ErrorIs(t, err, ErrPeerAlreadyExists) 50 | 51 | pcs := s.ListPeers() 52 | if assert.Len(t, pcs, 2) { 53 | var found [2]bool 54 | for _, pc := range pcs { 55 | if reflect.DeepEqual(pc, pcIPv4) { 56 | found[0] = true 57 | } 58 | if reflect.DeepEqual(pc, pcIPv6) { 59 | found[1] = true 60 | } 61 | } 62 | assert.True(t, found[0]) 63 | assert.True(t, found[1]) 64 | } 65 | 66 | err = s.DeletePeer(pcIPv4.RemoteAddress) 67 | assert.NoError(t, err) 68 | err = s.DeletePeer(pcIPv4.RemoteAddress) 69 | assert.ErrorIs(t, err, ErrPeerNotExist) 70 | } 71 | -------------------------------------------------------------------------------- /tcp_md5_sig.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package corebgp 5 | 6 | import ( 7 | "errors" 8 | "net/netip" 9 | ) 10 | 11 | // SetTCPMD5Signature sets a tcp md5 signature on a socket for the provided 12 | // address, prefix length, and key. This function is only supported on Linux. To 13 | // unset a signature provide an empty key. Prefix length is ignored on kernels 14 | // < 4.13. 15 | // 16 | // https://tools.ietf.org/html/rfc2385 17 | func SetTCPMD5Signature(fd int, address netip.Addr, prefixLen uint8, 18 | key string) error { 19 | return errors.New("unsupported") 20 | } 21 | -------------------------------------------------------------------------------- /tcp_md5_sig_linux.go: -------------------------------------------------------------------------------- 1 | package corebgp 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/netip" 7 | "unsafe" 8 | 9 | "golang.org/x/sys/unix" 10 | ) 11 | 12 | // https://github.com/torvalds/linux/blob/v5.11-rc7/include/uapi/linux/tcp.h#L326 13 | type tcpMD5Sig struct { 14 | ssFamily uint16 // https://github.com/torvalds/linux/blob/v5.11-rc7/include/uapi/linux/socket.h#L16 15 | ss [126]byte 16 | flags uint8 17 | prefixLen uint8 18 | keyLen uint16 19 | ifIndex uint32 // nolint: structcheck 20 | key [80]byte 21 | } 22 | 23 | func newTCPMD5Sig(fd int, address netip.Addr, prefixLen uint8, key string) ( 24 | tcpMD5Sig, error) { 25 | t := tcpMD5Sig{ 26 | flags: unix.TCP_MD5SIG_FLAG_PREFIX, 27 | } 28 | if len(key) > unix.TCP_MD5SIG_MAXKEYLEN { 29 | return t, fmt.Errorf("md5 key len is > %d", 30 | unix.TCP_MD5SIG_MAXKEYLEN) 31 | } 32 | sa, err := unix.Getsockname(fd) 33 | if err != nil { 34 | return t, err 35 | } 36 | switch sa.(type) { 37 | case *unix.SockaddrInet4: 38 | if !address.Is4() { 39 | // we can only set a key for an ipv4 addr on an af_inet socket 40 | return t, errors.New("invalid address") 41 | } 42 | t.ssFamily = unix.AF_INET 43 | copy(t.ss[2:], address.AsSlice()) 44 | case *unix.SockaddrInet6: 45 | t.ssFamily = unix.AF_INET6 46 | if !address.IsValid() { 47 | // https://github.com/torvalds/linux/blob/v5.11-rc7/net/ipv6/tcp_ipv6.c#L636-L640 48 | // 49 | // address may be ipv4 or ipv6 for an AF_INET6 wildcard socket. 50 | return t, errors.New("invalid address") 51 | } 52 | // ensure address is represented as 16 bytes as ipv4-mapped ipv6 is 53 | // valid here 54 | as16 := address.As16() 55 | copy(t.ss[6:], as16[:]) 56 | default: 57 | return t, errors.New("unknown socket type") 58 | } 59 | t.prefixLen = prefixLen 60 | t.keyLen = uint16(len(key)) 61 | copy(t.key[0:], []byte(key)) 62 | return t, nil 63 | } 64 | 65 | // SetTCPMD5Signature sets a tcp md5 signature on a socket for the provided 66 | // address, prefix length, and key. This function is only supported on Linux. To 67 | // unset a signature provide an empty key. Prefix length is ignored on kernels 68 | // < 4.13. 69 | // 70 | // https://tools.ietf.org/html/rfc2385 71 | func SetTCPMD5Signature(fd int, address netip.Addr, prefixLen uint8, 72 | key string) error { 73 | t, err := newTCPMD5Sig(fd, address, prefixLen, key) 74 | if err != nil { 75 | return err 76 | } 77 | b := *(*[unsafe.Sizeof(t)]byte)(unsafe.Pointer(&t)) 78 | return unix.SetsockoptString(fd, unix.IPPROTO_TCP, unix.TCP_MD5SIG_EXT, 79 | string(b[:])) 80 | } 81 | -------------------------------------------------------------------------------- /tcp_md5_sig_linux_test.go: -------------------------------------------------------------------------------- 1 | package corebgp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/netip" 8 | "syscall" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestSetTCPMD5Signature(t *testing.T) { 14 | // setup AF_INET wildcard socket 15 | lis, err := net.Listen("tcp4", ":0") 16 | if err != nil { 17 | t.Fatalf("error listening: %v", err) 18 | } 19 | defer lis.Close() 20 | _, port, err := net.SplitHostPort(lis.Addr().String()) 21 | if err != nil { 22 | t.Fatalf("error splitting host/port: %v", err) 23 | } 24 | tlis, ok := lis.(*net.TCPListener) 25 | if !ok { 26 | t.Fatal("not tcp listener") 27 | } 28 | raw, err := tlis.SyscallConn() 29 | if err != nil { 30 | t.Fatalf("error getting raw conn: %v", err) 31 | } 32 | 33 | // set key w/nil addr, this should fail 34 | var seterr error 35 | err = raw.Control(func(fdPtr uintptr) { 36 | fd := int(fdPtr) 37 | // nil address 38 | seterr = SetTCPMD5Signature(fd, netip.Addr{}, 32, 39 | "password") 40 | }) 41 | if err != nil { 42 | t.Fatalf("control err: %v", err) 43 | } 44 | if seterr == nil { 45 | t.Fatal("nil address should fail") 46 | } 47 | 48 | // set ipv6 addr on AF_INET socket, this should fail 49 | err = raw.Control(func(fdPtr uintptr) { 50 | fd := int(fdPtr) 51 | // ipv6 address 52 | seterr = SetTCPMD5Signature(fd, netip.MustParseAddr("2001:db8::1"), 53 | 128, "password") 54 | }) 55 | if err != nil { 56 | t.Fatalf("control err: %v", err) 57 | } 58 | if seterr == nil { 59 | t.Fatal("ipv6 address on ipv4 socket should fail") 60 | } 61 | 62 | // set valid ipv4 addr/key on AF_INET socket 63 | err = raw.Control(func(fdPtr uintptr) { 64 | fd := int(fdPtr) 65 | // valid 66 | seterr = SetTCPMD5Signature(fd, netip.MustParseAddr("127.0.0.1"), 67 | 32, "password") 68 | }) 69 | if err != nil { 70 | t.Fatalf("control err: %v", err) 71 | } 72 | if seterr != nil { 73 | t.Fatalf("unexpected error: %v", err) 74 | } 75 | 76 | // dial w/password from previously set addr, this should succeed 77 | laddr, err := net.ResolveTCPAddr("tcp", 78 | net.JoinHostPort("127.0.0.1", "0")) 79 | if err != nil { 80 | t.Fatalf("error resolving laddr: %v", err) 81 | } 82 | dialer := &net.Dialer{ 83 | Timeout: time.Second, 84 | LocalAddr: laddr, 85 | Control: func(network, address string, c syscall.RawConn) error { 86 | err := c.Control(func(fdPtr uintptr) { 87 | fd := int(fdPtr) 88 | seterr = SetTCPMD5Signature(fd, netip.MustParseAddr("127.0.0.1"), 89 | 32, "password") 90 | }) 91 | if err != nil { 92 | return err 93 | } 94 | return seterr 95 | }, 96 | } 97 | conn, err := dialer.Dial("tcp", fmt.Sprintf("127.0.0.1:%s", 98 | port)) 99 | if err != nil { 100 | t.Fatalf("error dialing w/md5: %v", err) 101 | } 102 | defer conn.Close() 103 | 104 | // unset previously set password 105 | err = raw.Control(func(fdPtr uintptr) { 106 | fd := int(fdPtr) 107 | // unset 108 | seterr = SetTCPMD5Signature(fd, netip.MustParseAddr("127.0.0.1"), 109 | 32, "") 110 | }) 111 | if err != nil { 112 | t.Fatalf("control err: %v", err) 113 | } 114 | if seterr != nil { 115 | t.Fatalf("error unsetting: %v", err) 116 | } 117 | 118 | // dial w/o password, this should succeed 119 | conn, err = net.Dial("tcp", fmt.Sprintf("127.0.0.1:%s", port)) 120 | if err != nil { 121 | t.Fatalf("error dialing w/o md5: %v", err) 122 | } 123 | defer conn.Close() 124 | 125 | // create wildcard dual stack socket and set password for IPv4 addr, this 126 | // should succeed 127 | lc := &net.ListenConfig{ 128 | Control: func(network, address string, c syscall.RawConn) error { 129 | err := c.Control(func(fdPtr uintptr) { 130 | fd := int(fdPtr) 131 | seterr = SetTCPMD5Signature(fd, netip.MustParseAddr("127.0.0.1"), 132 | 32, "password") 133 | }) 134 | if err != nil { 135 | return err 136 | } 137 | return seterr 138 | }, 139 | } 140 | lis, err = lc.Listen(context.Background(), "tcp", 141 | net.JoinHostPort("::", "0")) 142 | if err != nil { 143 | t.Fatalf("error listening: %v", err) 144 | } 145 | defer lis.Close() 146 | _, port, err = net.SplitHostPort(lis.Addr().String()) 147 | if err != nil { 148 | t.Fatalf("error splitting host/port: %v", err) 149 | } 150 | laddr, err = net.ResolveTCPAddr("tcp", 151 | net.JoinHostPort("127.0.0.1", "0")) 152 | if err != nil { 153 | t.Fatalf("error resolving laddr: %v", err) 154 | } 155 | 156 | // dial the wildcard dual stack socket using IPv4 and previously set 157 | // password, this should succeed 158 | dialer = &net.Dialer{ 159 | Timeout: time.Second, 160 | LocalAddr: laddr, 161 | Control: func(network, address string, c syscall.RawConn) error { 162 | err := c.Control(func(fdPtr uintptr) { 163 | fd := int(fdPtr) 164 | seterr = SetTCPMD5Signature(fd, netip.MustParseAddr("127.0.0.1"), 165 | 32, "password") 166 | }) 167 | if err != nil { 168 | return err 169 | } 170 | return seterr 171 | }, 172 | } 173 | conn, err = dialer.Dial("tcp4", fmt.Sprintf("127.0.0.1:%s", 174 | port)) 175 | if err != nil { 176 | t.Fatalf("error dialing: %v", err) 177 | } 178 | defer conn.Close() 179 | } 180 | -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21 2 | ARG BIRD_VERSION=v2.0.12 3 | 4 | # utils & BIRD deps 5 | RUN apt-get update && \ 6 | apt-get install -y iputils-ping net-tools wget automake bison flex \ 7 | libncurses-dev libreadline-dev 8 | 9 | # install BIRD 10 | RUN cd /tmp && \ 11 | git clone https://gitlab.nic.cz/labs/bird.git && cd bird && \ 12 | git checkout tags/$BIRD_VERSION && \ 13 | autoreconf && \ 14 | ./configure --prefix=/usr --sysconfdir=/etc --runstatedir=/run/bird && \ 15 | make && make install 16 | 17 | EXPOSE 179/tcp -------------------------------------------------------------------------------- /test/bird_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package test 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "fmt" 9 | "io/ioutil" 10 | "net" 11 | "net/netip" 12 | "reflect" 13 | "regexp" 14 | "strings" 15 | "syscall" 16 | "testing" 17 | "time" 18 | 19 | "github.com/jwhited/corebgp" 20 | ) 21 | 22 | const ( 23 | myAddress = "192.0.2.1" 24 | birdAddress = "192.0.2.2" 25 | myAS = 65001 26 | birdAS = 65002 27 | ) 28 | 29 | type plugin struct { 30 | caps []corebgp.Capability 31 | openNotification *corebgp.Notification 32 | updateMessageHandler corebgp.UpdateMessageHandler 33 | event chan pluginEvent 34 | } 35 | 36 | type pluginEvent interface { 37 | at() time.Time 38 | peer() corebgp.PeerConfig 39 | } 40 | 41 | type baseEvent struct { 42 | t time.Time 43 | c corebgp.PeerConfig 44 | } 45 | 46 | func (b baseEvent) at() time.Time { 47 | return b.t 48 | } 49 | 50 | func (b baseEvent) peer() corebgp.PeerConfig { 51 | return b.c 52 | } 53 | 54 | type getCapsEvent struct { 55 | baseEvent 56 | } 57 | 58 | type onOpenEvent struct { 59 | baseEvent 60 | routerID netip.Addr 61 | caps []corebgp.Capability 62 | } 63 | 64 | type onEstablishedEvent struct { 65 | baseEvent 66 | writer corebgp.UpdateMessageWriter 67 | } 68 | 69 | type onUpdateEvent struct { 70 | baseEvent 71 | update []byte 72 | } 73 | 74 | type onCloseEvent struct { 75 | baseEvent 76 | } 77 | 78 | func (p *plugin) GetCapabilities(peer corebgp.PeerConfig) []corebgp.Capability { 79 | p.event <- getCapsEvent{ 80 | baseEvent: baseEvent{ 81 | t: time.Now(), 82 | c: peer, 83 | }, 84 | } 85 | return p.caps 86 | } 87 | 88 | func (p *plugin) OnOpenMessage(peer corebgp.PeerConfig, routerID netip.Addr, 89 | capabilities []corebgp.Capability) *corebgp.Notification { 90 | p.event <- onOpenEvent{ 91 | baseEvent: baseEvent{ 92 | t: time.Now(), 93 | c: peer, 94 | }, 95 | routerID: routerID, 96 | caps: capabilities, 97 | } 98 | return p.openNotification 99 | } 100 | 101 | func (p *plugin) OnEstablished(peer corebgp.PeerConfig, 102 | writer corebgp.UpdateMessageWriter) corebgp.UpdateMessageHandler { 103 | p.event <- onEstablishedEvent{ 104 | baseEvent: baseEvent{ 105 | t: time.Now(), 106 | c: peer, 107 | }, 108 | writer: writer, 109 | } 110 | return p.updateMessageHandler 111 | } 112 | 113 | func (p *plugin) OnClose(peer corebgp.PeerConfig) { 114 | p.event <- onCloseEvent{ 115 | baseEvent: baseEvent{ 116 | t: time.Now(), 117 | c: peer, 118 | }, 119 | } 120 | } 121 | 122 | func (p *plugin) wantGetCapsEvent(t *testing.T, pc corebgp.PeerConfig) getCapsEvent { 123 | got := <-p.event 124 | want, ok := got.(getCapsEvent) 125 | if !ok { 126 | t.Fatalf("want: getCapsEvent, got: %s", reflect.TypeOf(got)) 127 | } 128 | verifyPeerConfig(t, want, pc) 129 | return want 130 | } 131 | 132 | func (p *plugin) wantOnOpenEvent(t *testing.T, pc corebgp.PeerConfig) onOpenEvent { 133 | got := <-p.event 134 | want, ok := got.(onOpenEvent) 135 | if !ok { 136 | t.Fatalf("want: onOpenEvent, got: %s", reflect.TypeOf(got)) 137 | } 138 | verifyPeerConfig(t, want, pc) 139 | return want 140 | } 141 | 142 | func (p *plugin) wantOnEstablishedEvent(t *testing.T, pc corebgp.PeerConfig) onEstablishedEvent { 143 | got := <-p.event 144 | want, ok := got.(onEstablishedEvent) 145 | if !ok { 146 | t.Fatalf("want: onEstablishedEvent, got: %s", reflect.TypeOf(got)) 147 | } 148 | verifyPeerConfig(t, want, pc) 149 | return want 150 | } 151 | 152 | func (p *plugin) wantOnUpdateEvent(t *testing.T, pc corebgp.PeerConfig) onUpdateEvent { 153 | got := <-p.event 154 | want, ok := got.(onUpdateEvent) 155 | if !ok { 156 | t.Fatalf("want: onUpdateEvent, got: %s", reflect.TypeOf(got)) 157 | } 158 | verifyPeerConfig(t, want, pc) 159 | return want 160 | } 161 | 162 | func (p *plugin) wantOnCloseEvent(t *testing.T, pc corebgp.PeerConfig) onCloseEvent { 163 | got := <-p.event 164 | want, ok := got.(onCloseEvent) 165 | if !ok { 166 | t.Fatalf("want: onCloseEvent, got: %s", reflect.TypeOf(got)) 167 | } 168 | verifyPeerConfig(t, want, pc) 169 | return want 170 | } 171 | 172 | func verifyPeerConfig(t *testing.T, event pluginEvent, config corebgp.PeerConfig) { 173 | if !reflect.DeepEqual(event.peer(), config) { 174 | t.Fatalf("unexpected peer: %v", event.peer()) 175 | } 176 | } 177 | 178 | const configPath = "/etc/bird/bird.conf" 179 | 180 | func loadBIRDConfig(t *testing.T, config []byte) { 181 | birdControl(t, "disable all") 182 | err := ioutil.WriteFile(configPath, config, 0644) 183 | if err != nil { 184 | t.Fatalf("error writing bird config: %v", err) 185 | } 186 | if !strings.Contains( 187 | birdControl(t, "configure check"), 188 | "Configuration OK") { 189 | t.Fatal("configure check failed") 190 | } 191 | if !strings.Contains( 192 | birdControl(t, fmt.Sprintf(`configure "%s"`, configPath)), 193 | "Reconfigured") { 194 | t.Fatal("failed to reconfigure bird") 195 | } 196 | birdControl(t, "enable all") 197 | } 198 | 199 | func baseBIRDConfig(plus []byte) []byte { 200 | return append([]byte(` 201 | router id 192.0.2.2; 202 | protocol device { 203 | } 204 | protocol kernel { 205 | ipv4 { 206 | table master4; 207 | import all; 208 | export all; 209 | }; 210 | } 211 | `), plus...) 212 | } 213 | 214 | // TestCleanBGPSession exercises all plugin event handlers for a clean 215 | // (no errors/notifications) BGP session w/BIRD. OPEN message negotiation is 216 | // expected to succeed and UPDATE messages should flow. 217 | func TestCleanBGPSession(t *testing.T) { 218 | loadBIRDConfig(t, baseBIRDConfig([]byte(` 219 | protocol static { 220 | ipv4; 221 | route 10.0.0.0/8 via "eth0"; 222 | } 223 | protocol bgp corebgp { 224 | description "corebgp"; 225 | local 192.0.2.2 as 65002; 226 | neighbor 192.0.2.1 as 65001; 227 | hold time 90; 228 | ipv4 { 229 | import all; 230 | export where source ~ [ RTS_STATIC, RTS_BGP ]; 231 | }; 232 | ipv6 { 233 | import all; 234 | export where source ~ [ RTS_STATIC, RTS_BGP ]; 235 | }; 236 | } 237 | `))) 238 | // disable BGP session on BIRD side 239 | birdControl(t, "disable corebgp") 240 | 241 | eventCh := make(chan pluginEvent, 1000) 242 | onUpdateFn := func(peer corebgp.PeerConfig, update []byte) *corebgp.Notification { 243 | eventCh <- onUpdateEvent{ 244 | baseEvent: baseEvent{ 245 | t: time.Now(), 246 | c: peer, 247 | }, 248 | update: update, 249 | } 250 | return nil 251 | } 252 | 253 | p := &plugin{ 254 | caps: []corebgp.Capability{ 255 | corebgp.NewMPExtensionsCapability(corebgp.AFI_IPV4, corebgp.SAFI_UNICAST), 256 | corebgp.NewMPExtensionsCapability(corebgp.AFI_IPV6, corebgp.SAFI_UNICAST), 257 | }, 258 | openNotification: nil, 259 | updateMessageHandler: onUpdateFn, 260 | event: eventCh, 261 | } 262 | 263 | server, err := corebgp.NewServer(netip.MustParseAddr(myAddress)) 264 | if err != nil { 265 | t.Fatalf("error constructing server: %v", err) 266 | } 267 | 268 | pc := corebgp.PeerConfig{ 269 | RemoteAddress: netip.MustParseAddr(birdAddress), 270 | RemoteAS: birdAS, 271 | LocalAS: myAS, 272 | } 273 | 274 | err = server.AddPeer(pc, p, corebgp.WithLocalAddress(netip.MustParseAddr(myAddress))) 275 | if err != nil { 276 | t.Fatalf("error adding peer: %v", err) 277 | } 278 | 279 | // enable BGP session on BIRD side 280 | birdControl(t, "enable corebgp") 281 | 282 | lis, err := net.Listen("tcp", net.JoinHostPort(myAddress, "179")) 283 | if err != nil { 284 | t.Fatalf("error constructing listener: %v", err) 285 | } 286 | defer lis.Close() 287 | go server.Serve([]net.Listener{lis}) // nolint: errcheck 288 | defer server.Close() 289 | 290 | // verify GetCapabilities 291 | p.wantGetCapsEvent(t, pc) 292 | 293 | // verify OnOpenMessage 294 | onOpen := p.wantOnOpenEvent(t, pc) 295 | if onOpen.routerID != netip.MustParseAddr(birdAddress) { 296 | t.Errorf("expected router ID %s, got: %s", birdAddress, 297 | onOpen.routerID) 298 | } 299 | if len(onOpen.caps) < 2 { 300 | t.Errorf("expected at least 2 caps in open message, got: %d", 301 | len(onOpen.caps)) 302 | } 303 | for _, capA := range p.caps { 304 | found := false 305 | for _, capB := range onOpen.caps { 306 | if reflect.DeepEqual(capA, capB) { 307 | found = true 308 | break 309 | } 310 | } 311 | if !found { 312 | t.Errorf("capability not found in peer's open message: %v", 313 | capA) 314 | } 315 | } 316 | 317 | // verify OnEstablished 318 | oe := p.wantOnEstablishedEvent(t, pc) 319 | // send UPDATE to BIRD 320 | outboundUpdate := []byte{ 321 | 0x00, 0x00, // withdrawn routes length 322 | 0x00, 0x14, // total path attribute length 323 | 0x40, 0x01, 0x01, 0x00, // origin igp 324 | 0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xe9, // as_path 65001 325 | 0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x01, // next_hop 192.0.2.1 326 | 0x10, 0x0a, 0x00, // nlri 10.0.0.0/16 327 | } 328 | err = oe.writer.WriteUpdate(outboundUpdate) 329 | if err != nil { 330 | t.Fatalf("got error while sending update: %v", err) 331 | } 332 | 333 | // expect UPDATE containing 10.0.0.0/8 334 | ou := p.wantOnUpdateEvent(t, pc) 335 | want := []byte{ 336 | 0x00, 0x00, // withdrawn routes length 337 | 0x00, 0x14, // total path attribute length 338 | 0x40, 0x01, 0x01, 0x00, // origin igp 339 | 0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xea, // as_path 65002 340 | 0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x02, // next_hop 192.0.2.2 341 | 0x08, 0x0a, // nlri 10.0.0.0/8 342 | } 343 | if !bytes.Equal(want, ou.update) { 344 | t.Errorf("expected %s for IPv4 UPDATE, got: %v", want, ou.update) 345 | } 346 | 347 | // expect IPv4 End of RIB marker 348 | ou = p.wantOnUpdateEvent(t, pc) 349 | want = []byte{0, 0, 0, 0} 350 | if !bytes.Equal(want, ou.update) { 351 | t.Errorf("expected %s for IPv4 EoR, got: %v", want, ou.update) 352 | } 353 | 354 | // expect IPv6 End of RIB Marker 355 | ou = p.wantOnUpdateEvent(t, pc) 356 | // https://tools.ietf.org/html/rfc4724#section-2 357 | // An UPDATE message with no reachable Network Layer Reachability 358 | // Information (NLRI) and empty withdrawn NLRI is specified as the End- 359 | // of-RIB marker that can be used by a BGP speaker to indicate to its 360 | // peer the completion of the initial routing update after the session 361 | // is established. For the IPv4 unicast address family, the End-of-RIB 362 | // marker is an UPDATE message with the minimum length [BGP-4]. For any 363 | // other address family, it is an UPDATE message that contains only the 364 | // MP_UNREACH_NLRI attribute [BGP-MP] with no withdrawn routes for that 365 | // . 366 | want = []byte{ 367 | 0x00, 0x00, // withdrawn routes length 368 | 0x00, 0x06, // path attribute length 369 | 0x80, 0x0f, 0x03, 0x00, 0x02, 0x01, // path attribute mp unreach nlri 370 | } 371 | if !bytes.Equal(want, ou.update) { 372 | t.Errorf("expected %v for IPv6 EoR, got: %v", want, ou.update) 373 | } 374 | 375 | // verify route seen by BIRD 376 | // 377 | /* 378 | bird> show route all 10.0.0.0/16 379 | Table master4: 380 | 10.0.0.0/16 unicast [corebgp 21:53:24.644] ! (100) [AS65001i] 381 | via 192.0.2.1 on eth0 382 | Type: BGP univ 383 | BGP.origin: IGP 384 | BGP.as_path: 65001 385 | BGP.next_hop: 192.0.2.1 386 | BGP.local_pref: 100 387 | */ 388 | output := birdControl(t, "show route all 10.0.0.0/16") 389 | substrings := []string{ 390 | "10.0.0.0/16", 391 | "corebgp", 392 | "BGP.origin: IGP", 393 | "BGP.as_path: 65001", 394 | "BGP.next_hop: 192.0.2.1", 395 | "BGP.local_pref: 100", 396 | } 397 | for _, sub := range substrings { 398 | if !strings.Contains(output, sub) { 399 | t.Errorf("expected substring '%s' in '%s'", sub, output) 400 | } 401 | } 402 | 403 | // shutdown bird 404 | birdControl(t, "disable corebgp") 405 | 406 | // verify OnClose 407 | p.wantOnCloseEvent(t, pc) 408 | } 409 | 410 | // TestNotificationSentOnOpen exercises the OnOpenMessage() handler and returns 411 | // a non-nil NOTIFICATION message to be sent to BIRD. We expect BIRD to receive 412 | // the NOTIFICATION and enter an error state for the corebgp peer. 413 | func TestNotificationSentOnOpen(t *testing.T) { 414 | loadBIRDConfig(t, baseBIRDConfig([]byte(` 415 | protocol bgp corebgp { 416 | description "corebgp"; 417 | local 192.0.2.2 as 65002; 418 | neighbor 192.0.2.1 as 65001; 419 | hold time 90; 420 | ipv4 { 421 | import all; 422 | export none; 423 | }; 424 | } 425 | `))) 426 | // disable BGP session on BIRD side 427 | birdControl(t, "disable corebgp") 428 | 429 | eventCh := make(chan pluginEvent, 1000) 430 | notification := &corebgp.Notification{ 431 | Code: corebgp.NOTIF_CODE_OPEN_MESSAGE_ERR, 432 | Data: []byte{}, 433 | } 434 | 435 | p := &plugin{ 436 | caps: []corebgp.Capability{ 437 | corebgp.NewMPExtensionsCapability(corebgp.AFI_IPV4, corebgp.SAFI_UNICAST), 438 | }, 439 | openNotification: notification, 440 | updateMessageHandler: nil, 441 | event: eventCh, 442 | } 443 | 444 | server, err := corebgp.NewServer(netip.MustParseAddr(myAddress)) 445 | if err != nil { 446 | t.Fatalf("error constructing server: %v", err) 447 | } 448 | 449 | pc := corebgp.PeerConfig{ 450 | RemoteAddress: netip.MustParseAddr(birdAddress), 451 | RemoteAS: birdAS, 452 | LocalAS: myAS, 453 | } 454 | 455 | err = server.AddPeer(pc, p, corebgp.WithLocalAddress(netip.MustParseAddr(myAddress))) 456 | if err != nil { 457 | t.Fatalf("error adding peer: %v", err) 458 | } 459 | 460 | // enable BGP session on BIRD side 461 | birdControl(t, "enable corebgp") 462 | 463 | lis, err := net.Listen("tcp", net.JoinHostPort(myAddress, "179")) 464 | if err != nil { 465 | t.Fatalf("error constructing listener: %v", err) 466 | } 467 | defer lis.Close() 468 | go server.Serve([]net.Listener{lis}) // nolint: errcheck 469 | defer server.Close() 470 | 471 | // expect get caps event 472 | p.wantGetCapsEvent(t, pc) 473 | 474 | p.wantOnOpenEvent(t, pc) 475 | 476 | // verify BIRD received the notification 477 | // 478 | /* 479 | bird> show protocols corebgp 480 | Name Proto Table State Since Info 481 | corebgp BGP --- start 22:39:46.063 Idle Received: Invalid OPEN message 482 | */ 483 | output := birdControl(t, "show protocols corebgp") 484 | invalidOpen := "Received: Invalid OPEN message" 485 | if !strings.Contains(output, invalidOpen) { 486 | t.Fatalf("expected substring '%s' in '%s'", invalidOpen, output) 487 | } 488 | } 489 | 490 | // TestNotificationSentOnUpdate exercises the UpdateMessageHandler and returns a 491 | // non-nil NOTIFICATION message to be sent to BIRD. We expect BIRD to receive 492 | // the NOTIFICATION and enter an error state for the corebgp peer. 493 | func TestNotificationSentOnUpdate(t *testing.T) { 494 | loadBIRDConfig(t, baseBIRDConfig([]byte(` 495 | protocol bgp corebgp { 496 | description "corebgp"; 497 | local 192.0.2.2 as 65002; 498 | neighbor 192.0.2.1 as 65001; 499 | hold time 90; 500 | ipv4 { 501 | import all; 502 | export none; 503 | }; 504 | } 505 | `))) 506 | // disable BGP session on BIRD side 507 | birdControl(t, "disable corebgp") 508 | 509 | eventCh := make(chan pluginEvent, 1000) 510 | notification := &corebgp.Notification{ 511 | Code: corebgp.NOTIF_CODE_UPDATE_MESSAGE_ERR, 512 | Data: []byte{}, 513 | } 514 | onUpdateFn := func(peer corebgp.PeerConfig, update []byte) *corebgp.Notification { 515 | eventCh <- onUpdateEvent{ 516 | baseEvent: baseEvent{ 517 | t: time.Now(), 518 | c: peer, 519 | }, 520 | update: update, 521 | } 522 | return notification 523 | } 524 | p := &plugin{ 525 | caps: []corebgp.Capability{ 526 | corebgp.NewMPExtensionsCapability(corebgp.AFI_IPV4, corebgp.SAFI_UNICAST), 527 | }, 528 | openNotification: nil, 529 | updateMessageHandler: onUpdateFn, 530 | event: eventCh, 531 | } 532 | 533 | server, err := corebgp.NewServer(netip.MustParseAddr(myAddress)) 534 | if err != nil { 535 | t.Fatalf("error constructing server: %v", err) 536 | } 537 | 538 | pc := corebgp.PeerConfig{ 539 | RemoteAddress: netip.MustParseAddr(birdAddress), 540 | RemoteAS: birdAS, 541 | LocalAS: myAS, 542 | } 543 | 544 | err = server.AddPeer(pc, p, corebgp.WithLocalAddress(netip.MustParseAddr(myAddress))) 545 | if err != nil { 546 | t.Fatalf("error adding peer: %v", err) 547 | } 548 | 549 | // enable BGP session on BIRD side 550 | birdControl(t, "enable corebgp") 551 | 552 | lis, err := net.Listen("tcp", net.JoinHostPort(myAddress, "179")) 553 | if err != nil { 554 | t.Fatalf("error constructing listener: %v", err) 555 | } 556 | defer lis.Close() 557 | go server.Serve([]net.Listener{lis}) // nolint: errcheck 558 | defer server.Close() 559 | 560 | // expect get caps event 561 | p.wantGetCapsEvent(t, pc) 562 | 563 | // expect on open event 564 | p.wantOnOpenEvent(t, pc) 565 | 566 | // expect on established event 567 | p.wantOnEstablishedEvent(t, pc) 568 | 569 | // expect on update event 570 | p.wantOnUpdateEvent(t, pc) 571 | 572 | // verify BIRD received the notification 573 | // 574 | /* 575 | bird> show protocols corebgp 576 | Name Proto Table State Since Info 577 | corebgp BGP --- start 22:57:50.627 Idle Received: Invalid UPDATE message 578 | */ 579 | output := birdControl(t, "show protocols corebgp") 580 | invalidUpdate := "Received: Invalid UPDATE message" 581 | if !strings.Contains(output, invalidUpdate) { 582 | t.Fatalf("expected substring '%s' in '%s'", invalidUpdate, output) 583 | } 584 | } 585 | 586 | // TestWithDialerControl exercises the WithDialerControl PeerOption by setting 587 | // a TCP MD5 signature on the dialing socket. 588 | func TestWithDialerControl(t *testing.T) { 589 | loadBIRDConfig(t, baseBIRDConfig([]byte(` 590 | protocol bgp corebgp { 591 | description "corebgp"; 592 | password "password"; 593 | local 192.0.2.2 as 65002; 594 | neighbor 192.0.2.1 as 65001; 595 | hold time 90; 596 | ipv4 { 597 | import all; 598 | export none; 599 | }; 600 | } 601 | `))) 602 | // disable BGP session on BIRD side 603 | birdControl(t, "disable corebgp") 604 | 605 | eventCh := make(chan pluginEvent, 1000) 606 | p := &plugin{ 607 | caps: []corebgp.Capability{ 608 | corebgp.NewMPExtensionsCapability(corebgp.AFI_IPV4, corebgp.SAFI_UNICAST), 609 | }, 610 | updateMessageHandler: nil, 611 | event: eventCh, 612 | } 613 | 614 | server, err := corebgp.NewServer(netip.MustParseAddr(myAddress)) 615 | if err != nil { 616 | t.Fatalf("error constructing server: %v", err) 617 | } 618 | 619 | pc := corebgp.PeerConfig{ 620 | RemoteAddress: netip.MustParseAddr(birdAddress), 621 | RemoteAS: birdAS, 622 | LocalAS: myAS, 623 | } 624 | 625 | err = server.AddPeer(pc, p, corebgp.WithLocalAddress(netip.MustParseAddr(myAddress)), 626 | corebgp.WithDialerControl(func(network, address string, c syscall.RawConn) error { 627 | var seterr error 628 | err := c.Control(func(fdPtr uintptr) { 629 | fd := int(fdPtr) 630 | seterr = corebgp.SetTCPMD5Signature(fd, pc.RemoteAddress, 631 | 32, "password") 632 | }) 633 | if err != nil { 634 | return err 635 | } 636 | return seterr 637 | })) 638 | if err != nil { 639 | t.Fatalf("error adding peer: %v", err) 640 | } 641 | 642 | // enable BGP session on BIRD side 643 | birdControl(t, "enable corebgp") 644 | 645 | // don't listen in order to ensure Dialer.Control is exercised 646 | go server.Serve(nil) // nolint: errcheck 647 | defer server.Close() 648 | 649 | // expect get caps event 650 | p.wantGetCapsEvent(t, pc) 651 | 652 | // expect on open event 653 | p.wantOnOpenEvent(t, pc) 654 | 655 | // expect on established event 656 | p.wantOnEstablishedEvent(t, pc) 657 | } 658 | 659 | func TestBIRDControl(t *testing.T) { 660 | birdControl(t, "show protocols all") 661 | } 662 | 663 | const ( 664 | controlSocket = "/run/bird/bird.ctl" 665 | ) 666 | 667 | var ( 668 | birdReadyPrefix = regexp.MustCompile(`^0001 BIRD.*ready.`) 669 | birdLinePrefix = regexp.MustCompile(`^[0-9]{4}[ \-]`) 670 | ) 671 | 672 | // birdControl connects to the BIRD unix socket, runs a command, and returns the 673 | // output. 674 | // 675 | // documentation on BIRD's unix socket: https://bird.network.cz/?get_doc&v=20&f=prog-2.html#ss2.9 676 | // 677 | // sample birdctl output: 678 | /* 679 | 0001 BIRD v2.0.7 ready. 680 | 2002-Name Proto Table State Since Info 681 | 1002-device1 Device --- up 21:35:14.093 682 | 1006- 683 | 1002-direct1 Direct --- down 21:35:14.093 684 | 1006- Channel ipv4 685 | State: DOWN 686 | Table: master4 687 | Preference: 240 688 | Input filter: ACCEPT 689 | Output filter: REJECT 690 | Channel ipv6 691 | State: DOWN 692 | Table: master6 693 | Preference: 240 694 | Input filter: ACCEPT 695 | Output filter: REJECT 696 | bird_test.go:82: 697 | 1002-kernel1 Kernel master4 up 21:35:14.093 698 | 1006- Channel ipv4 699 | State: UP 700 | Table: master4 701 | Preference: 10 702 | Input filter: ACCEPT 703 | Output filter: ACCEPT 704 | Routes: 0 imported, 0 exported, 0 preferred 705 | Route change stats: received rejected filtered ignored accepted 706 | Import updates: 0 0 0 0 0 707 | Import withdraws: 0 0 --- 0 0 708 | Export updates: 0 0 0 --- 0 709 | Export withdraws: 0 --- --- --- 0 710 | bird_test.go:82: 711 | 1002-kernel2 Kernel master6 up 21:35:14.093 712 | 1006- Channel ipv6 713 | State: UP 714 | Table: master6 715 | Preference: 10 716 | Input filter: ACCEPT 717 | Output filter: ACCEPT 718 | Routes: 0 imported, 0 exported, 0 preferred 719 | Route change stats: received rejected filtered ignored accepted 720 | Import updates: 0 0 0 0 0 721 | Import withdraws: 0 0 --- 0 0 722 | Export updates: 0 0 0 --- 0 723 | Export withdraws: 0 --- --- --- 0 724 | bird_test.go:82: 725 | 1002-static1 Static master4 up 21:35:14.093 726 | 1006- Channel ipv4 727 | State: UP 728 | Table: master4 729 | Preference: 200 730 | Input filter: ACCEPT 731 | Output filter: REJECT 732 | Routes: 0 imported, 0 exported, 0 preferred 733 | Route change stats: received rejected filtered ignored accepted 734 | Import updates: 0 0 0 0 0 735 | Import withdraws: 0 0 --- 0 0 736 | Export updates: 0 0 0 --- 0 737 | Export withdraws: 0 --- --- --- 0 738 | bird_test.go:82: 739 | 1002-corebgp BGP --- start 21:35:14.093 Active Socket: Connection refused 740 | 1006- Description: corebgp 741 | BGP state: Active 742 | Neighbor address: 192.0.2.1 743 | Neighbor AS: 65001 744 | Local AS: 65002 745 | Connect delay: 2.979/5 746 | Last error: Socket: Connection refused 747 | Channel ipv4 748 | State: DOWN 749 | Table: master4 750 | Preference: 100 751 | Input filter: ACCEPT 752 | Output filter: (unnamed) 753 | Channel ipv6 754 | State: DOWN 755 | Table: master6 756 | Preference: 100 757 | Input filter: ACCEPT 758 | Output filter: (unnamed) 759 | bird_test.go:82: 760 | 0000 761 | */ 762 | func birdControl(t *testing.T, command string) string { 763 | c, err := net.Dial("unix", controlSocket) 764 | if err != nil { 765 | t.Fatalf("error dialing UDS: %v", err) 766 | } 767 | defer c.Close() 768 | 769 | _, err = c.Write([]byte(fmt.Sprintf("%s\r\n", command))) 770 | if err != nil { 771 | t.Fatalf("error writing to UDS: %v", err) 772 | } 773 | 774 | var out strings.Builder 775 | scanner := bufio.NewScanner(c) 776 | first := true 777 | for scanner.Scan() { 778 | b := scanner.Bytes() 779 | if first { 780 | // BIRD spits out '0001 BIRD v2.0.7 ready.' upon connecting 781 | if !birdReadyPrefix.Match(b) { 782 | t.Fatalf("unexpected first line back from BIRD: %s", b) 783 | } 784 | first = false 785 | continue 786 | } 787 | if birdLinePrefix.Match(b) { 788 | // Requests are commands encoded as a single line of text, replies 789 | // are sequences of lines starting with a four-digit code followed 790 | // by either a space (if it's the last line of the reply) or a minus 791 | // sign (when the reply is going to continue with the next line) 792 | if b[4] == ' ' { 793 | out.Write(b[5:]) // sometimes the last line contains text 794 | break 795 | } 796 | b = b[5:] 797 | } 798 | out.Write(b) 799 | out.WriteByte('\n') 800 | } 801 | 802 | return out.String() 803 | } 804 | -------------------------------------------------------------------------------- /test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | corebgp: 4 | build: . 5 | working_dir: "/go/src/github.com/jwhited/corebgp/test" 6 | volumes: 7 | - "../:/go/src/github.com/jwhited/corebgp" 8 | - "bird-config:/etc/bird" 9 | - "bird-run:/run/bird" 10 | entrypoint: [ "go", "test", "-v", "-tags=integration", "-count=1", "./..." ] 11 | networks: 12 | bgp: 13 | ipv4_address: 192.0.2.1 14 | ipv6_address: "2001:DB8::1" 15 | 16 | bird: 17 | build: . 18 | volumes: 19 | - "bird-config:/etc/bird" 20 | - "bird-run:/run/bird" 21 | entrypoint: [ "/usr/sbin/bird", "-d", "-c", "/etc/bird.conf" ] 22 | networks: 23 | bgp: 24 | ipv4_address: 192.0.2.2 25 | ipv6_address: "2001:DB8::2" 26 | 27 | networks: 28 | bgp: 29 | enable_ipv6: true 30 | driver: bridge 31 | driver_opts: 32 | com.docker.network.enable_ipv6: "true" 33 | ipam: 34 | driver: default 35 | config: 36 | - subnet: 192.0.2.0/24 37 | gateway: 192.0.2.254 38 | - subnet: "2001:DB8::/64" 39 | gateway: "2001:DB8::254" 40 | 41 | volumes: 42 | bird-config: 43 | bird-run: -------------------------------------------------------------------------------- /testdata/fuzz/FuzzUpdateDecoder_Decode/0ea655d260c00382: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\x00\x00\x00\x03A00") 3 | -------------------------------------------------------------------------------- /testdata/fuzz/FuzzUpdateDecoder_Decode/c5c288406e9f4d1c: -------------------------------------------------------------------------------- 1 | go test fuzz v1 2 | []byte("\x00\x00\x00\x14A0\x00A0\x0200A0\x010AA\x0300000") 3 | -------------------------------------------------------------------------------- /update_test.go: -------------------------------------------------------------------------------- 1 | package corebgp 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/netip" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type updateMessageForTests struct { 14 | addPath bool 15 | withdrawn []netip.Prefix 16 | addPathWithdrawn []AddPathPrefix 17 | origin uint8 18 | asPath []uint32 19 | communities []uint32 20 | largeCommunities LargeCommunitiesPathAttr 21 | localPref uint32 22 | med uint32 23 | nextHop netip.Addr 24 | nlri []netip.Prefix 25 | addPathNLRI []AddPathPrefix 26 | ipv6NextHops []netip.Addr 27 | addPathIPv6NLRI []AddPathPrefix 28 | ipv6NLRI []netip.Prefix 29 | addPathIPv6Withdrawn []AddPathPrefix 30 | ipv6Withdrawn []netip.Prefix 31 | } 32 | 33 | func newPathAttrsDecodeFn() func(m *updateMessageForTests, code uint8, flags PathAttrFlags, b []byte) error { 34 | reachDecodeFn := NewMPReachNLRIDecodeFn[*updateMessageForTests]( 35 | func(m *updateMessageForTests, afi uint16, safi uint8, nh, nlri []byte) error { 36 | if afi == AFI_IPV6 && safi == SAFI_UNICAST { 37 | nhs, err := DecodeMPReachIPv6NextHops(nh) 38 | if err != nil { 39 | return err 40 | } 41 | if m.addPath { 42 | prefixes, err := DecodeMPIPv6AddPathPrefixes(nlri) 43 | if err != nil { 44 | return err 45 | } 46 | m.addPathIPv6NLRI = prefixes 47 | } else { 48 | prefixes, err := DecodeMPIPv6Prefixes(nlri) 49 | if err != nil { 50 | return err 51 | } 52 | m.ipv6NLRI = prefixes 53 | } 54 | m.ipv6NextHops = nhs 55 | } 56 | return nil 57 | }, 58 | ) 59 | unreachDecodeFn := NewMPUnreachNLRIDecodeFn[*updateMessageForTests]( 60 | func(m *updateMessageForTests, afi uint16, safi uint8, withdrawn []byte) error { 61 | if afi == AFI_IPV6 && safi == SAFI_UNICAST { 62 | if m.addPath { 63 | prefixes, err := DecodeMPIPv6AddPathPrefixes(withdrawn) 64 | if err != nil { 65 | return err 66 | } 67 | m.addPathIPv6Withdrawn = prefixes 68 | } else { 69 | prefixes, err := DecodeMPIPv6Prefixes(withdrawn) 70 | if err != nil { 71 | return err 72 | } 73 | m.ipv6Withdrawn = prefixes 74 | } 75 | } 76 | return nil 77 | }, 78 | ) 79 | return func(m *updateMessageForTests, code uint8, flags PathAttrFlags, b []byte) error { 80 | switch code { 81 | case PATH_ATTR_ORIGIN: 82 | var o OriginPathAttr 83 | err := o.Decode(flags, b) 84 | if err != nil { 85 | return err 86 | } 87 | m.origin = uint8(o) 88 | return nil 89 | case PATH_ATTR_AS_PATH: 90 | var a ASPathAttr 91 | err := a.Decode(flags, b) 92 | if err != nil { 93 | return err 94 | } 95 | m.asPath = a.ASSequence 96 | return nil 97 | case PATH_ATTR_NEXT_HOP: 98 | var nh NextHopPathAttr 99 | err := nh.Decode(flags, b) 100 | if err != nil { 101 | return err 102 | } 103 | m.nextHop = netip.Addr(nh) 104 | return nil 105 | case PATH_ATTR_COMMUNITY: 106 | var comms CommunitiesPathAttr 107 | err := comms.Decode(flags, b) 108 | if err != nil { 109 | return err 110 | } 111 | m.communities = comms 112 | case PATH_ATTR_LOCAL_PREF: 113 | var lpref LocalPrefPathAttr 114 | if err := lpref.Decode(flags, b); err != nil { 115 | fmt.Printf("error decoding local pref: %v", err) 116 | return err 117 | } 118 | m.localPref = uint32(lpref) 119 | case PATH_ATTR_LARGE_COMMUNITY: 120 | var lc LargeCommunitiesPathAttr 121 | if err := lc.Decode(flags, b); err != nil { 122 | return err 123 | } 124 | m.largeCommunities = lc 125 | case PATH_ATTR_MED: 126 | var med MEDPathAttr 127 | if err := med.Decode(flags, b); err != nil { 128 | fmt.Printf("error decoding med: %v", err) 129 | return err 130 | } 131 | m.med = uint32(med) 132 | case PATH_ATTR_MP_REACH_NLRI: 133 | return reachDecodeFn(m, flags, b) 134 | case PATH_ATTR_MP_UNREACH_NLRI: 135 | return unreachDecodeFn(m, flags, b) 136 | case PATH_ATTR_ATOMIC_AGGREGATE: 137 | var aa AtomicAggregatePathAttr 138 | return aa.Decode(flags, b) 139 | } 140 | return nil 141 | } 142 | } 143 | 144 | func FuzzUpdateDecoder_Decode(f *testing.F) { 145 | f.Add([]byte{ 146 | 0x00, 0x03, // withdrawn routes length 147 | 0x10, 0x0a, 0x00, // withdrawn 10.0.0.0/16 148 | 0x00, 0x14, // total path attribute length 149 | 0x40, 0x01, 0x01, 0x01, // origin egp 150 | 0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xea, // as_path 65002 151 | 0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x02, // next_hop 192.0.2.2 152 | 0x08, 0x0a, // nlri 10.0.0.0/8 153 | }) 154 | f.Add([]byte{ 155 | 0x00, 0x07, // withdrawn routes length 156 | 0x00, 0x00, 0x00, 0x01, 0x10, 0x0a, 0x00, // withdrawn id 1 10.0.0.0/16 157 | 0x00, 0x14, // total path attribute length 158 | 0x40, 0x01, 0x01, 0x01, // origin egp 159 | 0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xea, // as_path 65002 160 | 0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x02, // next_hop 192.0.2.2 161 | 0x00, 0x00, 0x00, 0x02, 0x08, 0x0a, // nlri id 2 10.0.0.0/8 162 | }) 163 | f.Add([]byte{ 164 | 0x00, 0x00, // withdrawn routes length 165 | 0x00, 0x3f, // total path attribute length 166 | // extended len MP_REACH_NLRI 2001:db8::/64 nhs 2001:db8::2 & fe80::42:c0ff:fe00:202 167 | 0x90, 0x0e, 0x00, 0x2e, 0x00, 0x02, 0x01, 0x20, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0xc0, 0xff, 0xfe, 0x00, 0x02, 0x02, 0x00, 0x40, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 168 | 0x40, 0x01, 0x01, 0x00, // origin igp 169 | 0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xea, // as_path 65002 170 | }) 171 | f.Add([]byte{ 172 | 0x00, 0x00, // withdrawn routes length 173 | 0x00, 0x09, // total path attribute length 174 | 0x90, 0x0f, 0x00, 0x05, 0x00, 0x02, 0x01, 0x07, 0xfc, // empty IPv6 MP_UNREACH_NLRI 175 | }) 176 | f.Fuzz(func(t *testing.T, b []byte) { 177 | ud := NewUpdateDecoder[*updateMessageForTests]( 178 | NewWithdrawnRoutesDecodeFn(func(m *updateMessageForTests, r []netip.Prefix) error { 179 | m.withdrawn = r 180 | return nil 181 | }), 182 | newPathAttrsDecodeFn(), 183 | NewNLRIDecodeFn(func(m *updateMessageForTests, r []netip.Prefix) error { 184 | m.nlri = r 185 | return nil 186 | }), 187 | ) 188 | m := &updateMessageForTests{} 189 | ud.Decode(m, b) 190 | }) 191 | } 192 | 193 | func newWithdrawnRoutesDecodeFn() DecodeFn[*updateMessageForTests] { 194 | fn := NewWithdrawnRoutesDecodeFn[*updateMessageForTests](func(u *updateMessageForTests, p []netip.Prefix) error { 195 | u.withdrawn = p 196 | return nil 197 | }) 198 | apFn := NewWithdrawnAddPathRoutesDecodeFn[*updateMessageForTests](func(u *updateMessageForTests, a []AddPathPrefix) error { 199 | u.addPathWithdrawn = a 200 | return nil 201 | }) 202 | return func(u *updateMessageForTests, b []byte) error { 203 | if u.addPath { 204 | return apFn(u, b) 205 | } 206 | return fn(u, b) 207 | } 208 | } 209 | 210 | func newNLRIDecodeFn() DecodeFn[*updateMessageForTests] { 211 | fn := NewNLRIDecodeFn[*updateMessageForTests](func(u *updateMessageForTests, p []netip.Prefix) error { 212 | u.nlri = p 213 | return nil 214 | }) 215 | apFn := NewNLRIAddPathDecodeFn[*updateMessageForTests](func(u *updateMessageForTests, a []AddPathPrefix) error { 216 | u.addPathNLRI = a 217 | return nil 218 | }) 219 | return func(u *updateMessageForTests, b []byte) error { 220 | if u.addPath { 221 | return apFn(u, b) 222 | } 223 | return fn(u, b) 224 | } 225 | } 226 | 227 | func TestUpdateDecoder_Decode(t *testing.T) { 228 | ud := NewUpdateDecoder[*updateMessageForTests]( 229 | newWithdrawnRoutesDecodeFn(), 230 | newPathAttrsDecodeFn(), 231 | newNLRIDecodeFn(), 232 | ) 233 | 234 | t.Run("valid data", func(t *testing.T) { 235 | cases := []struct { 236 | name string 237 | toDecode []byte 238 | want *updateMessageForTests 239 | }{ 240 | { 241 | name: "IPv4 withdrawn and IPv4 nlri", 242 | toDecode: []byte{ 243 | 0x00, 0x05, // withdrawn routes length 244 | 0x10, 0x0a, 0x00, // withdrawn 10.0.0.0/16 245 | 0x08, 0x01, // withdrawn 1.0.0.0/8 246 | 0x00, 0x14, // total path attribute length 247 | 0x40, 0x01, 0x01, 0x01, // origin egp 248 | 0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xea, // as_path 65002 249 | 0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x02, // next_hop 192.0.2.2 250 | 0x08, 0x0a, // nlri 10.0.0.0/8 251 | 0x18, 0x01, 0x00, 0x00, // nlri 1.0.0.0/24 252 | }, 253 | want: &updateMessageForTests{ 254 | withdrawn: []netip.Prefix{ 255 | netip.MustParsePrefix("10.0.0.0/16"), 256 | netip.MustParsePrefix("1.0.0.0/8"), 257 | }, 258 | origin: 1, 259 | asPath: []uint32{65002}, 260 | nextHop: netip.MustParseAddr("192.0.2.2"), 261 | nlri: []netip.Prefix{ 262 | netip.MustParsePrefix("10.0.0.0/8"), 263 | netip.MustParsePrefix("1.0.0.0/24"), 264 | }, 265 | }, 266 | }, 267 | { 268 | name: "add-path IPv4 withdrawn and IPv4 nlri", 269 | toDecode: []byte{ 270 | 0x00, 0x0f, // withdrawn routes length 271 | 0x00, 0x00, 0x00, 0x01, 0x10, 0x0a, 0x00, // withdrawn id 1 10.0.0.0/16 272 | 0x00, 0x00, 0x00, 0x02, 0x18, 0x0a, 0x00, 0x00, // withdrawn id 2 10.0.0.0/24 273 | 0x00, 0x14, // total path attribute length 274 | 0x40, 0x01, 0x01, 0x01, // origin egp 275 | 0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xea, // as_path 65002 276 | 0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x02, // next_hop 192.0.2.2 277 | 0x00, 0x00, 0x00, 0x02, 0x08, 0x0a, // nlri id 2 10.0.0.0/8 278 | }, 279 | want: &updateMessageForTests{ 280 | addPath: true, 281 | addPathWithdrawn: []AddPathPrefix{ 282 | {netip.MustParsePrefix("10.0.0.0/16"), 1}, 283 | {netip.MustParsePrefix("10.0.0.0/24"), 2}, 284 | }, 285 | origin: 1, 286 | asPath: []uint32{65002}, 287 | nextHop: netip.MustParseAddr("192.0.2.2"), 288 | addPathNLRI: []AddPathPrefix{ 289 | {netip.MustParsePrefix("10.0.0.0/8"), 2}, 290 | }, 291 | }, 292 | }, 293 | { 294 | name: "MP_UNREACH_NLRI IPv6 prefixes", 295 | toDecode: []byte{ 296 | 0x00, 0x00, // withdrawn routes length 297 | 0x00, 0x0b, // total path attribute length 298 | 0x90, 0x0f, 0x00, 0x07, 0x00, 0x02, 0x01, 0x07, 0xfc, 0x07, 0xfa, // extended len IPv6 MP_UNREACH_NLRI fc00::/7 fa00::/7 299 | }, 300 | want: &updateMessageForTests{ 301 | ipv6Withdrawn: []netip.Prefix{ 302 | netip.MustParsePrefix("fc00::/7"), 303 | netip.MustParsePrefix("fa00::/7"), 304 | }, 305 | }, 306 | }, 307 | { 308 | name: "add-path MP_UNREACH_NLRI IPv6 prefixes", 309 | toDecode: []byte{ 310 | 0x00, 0x00, // withdrawn routes length 311 | 0x00, 0x13, // total path attribute length 312 | 0x90, 0x0f, 0x00, 0x0f, 0x00, 0x02, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0xfc, 0x00, 0x00, 0x00, 0x02, 0x07, 0xfa, // extended len IPv6 MP_UNREACH_NLRI id 1 fc00::/7 id 2 fa00::/7 313 | }, 314 | want: &updateMessageForTests{ 315 | addPath: true, 316 | addPathIPv6Withdrawn: []AddPathPrefix{ 317 | { 318 | Prefix: netip.MustParsePrefix("fc00::/7"), 319 | ID: 1, 320 | }, 321 | { 322 | Prefix: netip.MustParsePrefix("fa00::/7"), 323 | ID: 2, 324 | }, 325 | }, 326 | }, 327 | }, 328 | { 329 | name: "MP_UNREACH_NLRI IPv6 end-of-rib", 330 | toDecode: []byte{ 331 | 0x00, 0x00, // withdrawn routes length 332 | 0x00, 0x06, // total path attribute length 333 | 0x80, 0x0f, 0x03, 0x00, 0x02, 0x01, // optional MP_UNREACH_NLRI len 3 334 | }, 335 | want: &updateMessageForTests{}, 336 | }, 337 | { 338 | name: "add-path MP_REACH_NLRI IPv6 prefixes", 339 | toDecode: []byte{ 340 | 0x00, 0x00, // withdrawn routes length 341 | 0x00, 0x50, // total path attribute length 342 | // extended len MP_REACH_NLRI 343 | 0x90, 0x0e, 0x00, 0x3F, 0x00, 0x02, 0x01, 0x20, 344 | // nh 2001:db8::2 345 | 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 346 | // nh fe80::42:c0ff:fe00:202 347 | 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0xc0, 0xff, 0xfe, 0x00, 0x02, 0x02, 348 | 0x00, // reserved 349 | // id 1 2001:db8::/64 350 | 0x00, 0x00, 0x00, 0x01, 0x40, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 351 | // id 2 2001:db9::/64 352 | 0x00, 0x00, 0x00, 0x02, 0x40, 0x20, 0x01, 0x0d, 0xb9, 0x00, 0x00, 0x00, 0x00, 353 | 0x40, 0x01, 0x01, 0x00, // origin igp 354 | 0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xea, // as_path 65002 355 | }, 356 | want: &updateMessageForTests{ 357 | addPath: true, 358 | asPath: []uint32{65002}, 359 | addPathIPv6NLRI: []AddPathPrefix{ 360 | { 361 | Prefix: netip.MustParsePrefix("2001:db8::/64"), 362 | ID: 1, 363 | }, 364 | { 365 | Prefix: netip.MustParsePrefix("2001:db9::/64"), 366 | ID: 2, 367 | }, 368 | }, 369 | ipv6NextHops: []netip.Addr{ 370 | netip.MustParseAddr("2001:db8::2"), 371 | netip.MustParseAddr("fe80::42:c0ff:fe00:202"), 372 | }, 373 | }, 374 | }, 375 | { 376 | name: "MP_REACH_NLRI IPv6 prefix", 377 | toDecode: []byte{ 378 | 0x00, 0x00, // withdrawn routes length 379 | 0x00, 0x48, // total path attribute length 380 | // extended len MP_REACH_NLRI 381 | 0x90, 0x0e, 0x00, 0x37, 0x00, 0x02, 0x01, 0x20, 382 | // nh 2001:db8::2 383 | 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 384 | // nh fe80::42:c0ff:fe00:202 385 | 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0xc0, 0xff, 0xfe, 0x00, 0x02, 0x02, 386 | 0x00, // reserved 387 | // 2001:db8::/64 388 | 0x40, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 389 | // 2001:db9::/64 390 | 0x40, 0x20, 0x01, 0x0d, 0xb9, 0x00, 0x00, 0x00, 0x00, 391 | 0x40, 0x01, 0x01, 0x00, // origin igp 392 | 0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xea, // as_path 65002 393 | }, 394 | want: &updateMessageForTests{ 395 | asPath: []uint32{65002}, 396 | ipv6NLRI: []netip.Prefix{ 397 | netip.MustParsePrefix("2001:db8::/64"), 398 | netip.MustParsePrefix("2001:db9::/64"), 399 | }, 400 | ipv6NextHops: []netip.Addr{ 401 | netip.MustParseAddr("2001:db8::2"), 402 | netip.MustParseAddr("fe80::42:c0ff:fe00:202"), 403 | }, 404 | }, 405 | }, 406 | { 407 | name: "add-path bird bug", 408 | toDecode: []byte{ 409 | 0x00, 0x00, // withdrawn routes length 410 | 0x00, 0x14, // total path attribute length 411 | 0x40, 0x01, 0x01, 0x00, // origin igp 412 | 0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xea, // as path 65002 413 | 0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x02, // next hop 192.0.2.2 414 | 0x00, 0x00, 0x00, 0x06, 0x18, 0xc0, 0x00, 0x02, // id 6 192.0.2.0/24 415 | }, 416 | want: &updateMessageForTests{ 417 | addPath: true, 418 | asPath: []uint32{65002}, 419 | nextHop: netip.MustParseAddr("192.0.2.2"), 420 | addPathNLRI: []AddPathPrefix{ 421 | { 422 | Prefix: netip.MustParsePrefix("192.0.2.0/24"), 423 | ID: 6, 424 | }, 425 | }, 426 | }, 427 | }, 428 | { 429 | name: "decode lpref, med, communities, large communities", 430 | toDecode: []byte{ 431 | 0x00, 0x00, // withdrawn routes length 432 | 0x00, 0x40, // total path attribute length 433 | 0x40, 0x01, 0x01, 0x00, // origin igp 434 | 0x40, 0x02, 0x0a, 0x02, 0x02, 0x00, 0x00, 0xfe, 0x4c, 0x00, 0x00, 0xfe, 0x4c, // as_path 65100 65100 435 | 0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x02, // next_hop 192.0.2.2 436 | 0x80, 0x04, 0x04, 0x00, 0x00, 0x00, 0x62, // med 98 437 | 0x40, 0x05, 0x04, 0x00, 0x00, 0x00, 0x32, // local pref 50 438 | 0xc0, 0x08, 0x08, 0x00, 0x64, 0x00, 0xc8, 0x01, 0x2c, 0x01, 0x90, // communities 100:200 300:400 439 | 0xc0, 0x20, 0x0c, 0x00, 0x00, 0xfe, 0x50, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // large-communities 65104:1:1 440 | 0x18, 0xc6, 0x33, 0x64, // nlri 198.51.100.0/24 441 | }, 442 | want: &updateMessageForTests{ 443 | asPath: []uint32{65100, 65100}, 444 | nextHop: netip.MustParseAddr("192.0.2.2"), 445 | med: 98, 446 | localPref: 50, 447 | communities: []uint32{6553800, 19661200}, 448 | largeCommunities: LargeCommunitiesPathAttr{ 449 | { 450 | GlobalAdmin: 65104, 451 | LocalData1: 1, 452 | LocalData2: 1, 453 | }, 454 | }, 455 | nlri: []netip.Prefix{ 456 | netip.MustParsePrefix("198.51.100.0/24"), 457 | }, 458 | }, 459 | }, 460 | } 461 | for _, tt := range cases { 462 | t.Run(tt.name, func(t *testing.T) { 463 | m := &updateMessageForTests{} 464 | m.addPath = tt.want.addPath 465 | n := ud.Decode(m, tt.toDecode) 466 | if n != nil { 467 | t.Fatalf("error decoding: %v", n) 468 | } 469 | if !reflect.DeepEqual(tt.want, m) { 470 | t.Fatalf("want: %+v != got: %+v", tt.want, m) 471 | } 472 | }) 473 | } 474 | }) 475 | 476 | t.Run("invalid data", func(t *testing.T) { 477 | cases := []struct { 478 | name string 479 | toDecode []byte 480 | wantNotification bool 481 | wantAsWithdraw bool 482 | wantAttrDiscard bool 483 | }{ 484 | { 485 | name: "less than 4 bytes", 486 | toDecode: []byte{ 487 | 0x00, 0x03, // withdrawn routes length 488 | }, 489 | wantNotification: true, 490 | }, 491 | { 492 | name: "missing origin", 493 | toDecode: []byte{ 494 | 0x00, 0x03, // withdrawn routes length 495 | 0x10, 0x0a, 0x00, // withdrawn 10.0.0.0/16 496 | 0x00, 0x10, // total path attribute length 497 | 0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xea, // as_path 65002 498 | 0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x02, // next_hop 192.0.2.2 499 | 0x08, 0x0a, // nlri 10.0.0.0/8 500 | }, 501 | wantAsWithdraw: true, 502 | }, 503 | { 504 | name: "nonzero atomic aggregate", 505 | toDecode: []byte{ 506 | 0x00, 0x03, // withdrawn routes length 507 | 0x10, 0x0a, 0x00, // withdrawn 10.0.0.0/16 508 | 0x00, 0x18, // total path attribute length 509 | 0x40, 0x01, 0x01, 0x01, // origin egp 510 | 0x40, 0x02, 0x06, 0x02, 0x01, 0x00, 0x00, 0xfd, 0xea, // as_path 65002 511 | 0x40, 0x03, 0x04, 0xc0, 0x00, 0x02, 0x02, // next_hop 192.0.2.2 512 | 0xc0, 0x06, 0x01, 0x01, // invalid atomic aggregate 513 | 0x08, 0x0a, // nlri 10.0.0.0/8 514 | }, 515 | wantAttrDiscard: true, 516 | }, 517 | } 518 | for _, tt := range cases { 519 | t.Run(tt.name, func(t *testing.T) { 520 | m := &updateMessageForTests{} 521 | n := ud.Decode(m, tt.toDecode) 522 | if n == nil { 523 | t.Fatal("Decode() returned nil") 524 | } 525 | var ( 526 | notif *Notification 527 | asWithdraw *TreatAsWithdrawUpdateErr 528 | attrDiscard *AttrDiscardUpdateErr 529 | ) 530 | if tt.wantNotification && !errors.As(n, ¬if) { 531 | t.Error("wanted notification error, none found") 532 | } 533 | if tt.wantAsWithdraw && !errors.As(n, &asWithdraw) { 534 | t.Error("wanted treat as withdraw error, none found") 535 | } 536 | if tt.wantAttrDiscard && !errors.As(n, &attrDiscard) { 537 | t.Error("wanted attr discard error, none found") 538 | } 539 | }) 540 | } 541 | }) 542 | } 543 | 544 | type fakeUpdateError struct { 545 | *Notification 546 | } 547 | 548 | func (f fakeUpdateError) AsSessionReset() *Notification { 549 | return f.Notification 550 | } 551 | 552 | func TestUpdateNotificationFromErr(t *testing.T) { 553 | type args struct { 554 | err error 555 | } 556 | tests := []struct { 557 | name string 558 | args args 559 | want *Notification 560 | }{ 561 | { 562 | name: "notification", 563 | args: args{ 564 | err: errors.Join( 565 | &Notification{Code: 1}, 566 | &Notification{Code: 2}, 567 | ), 568 | }, 569 | want: &Notification{Code: 1}, 570 | }, 571 | { 572 | name: "treat as withdraw", 573 | args: args{ 574 | err: errors.Join( 575 | &TreatAsWithdrawUpdateErr{Notification: &Notification{Code: 1}}, 576 | &AttrDiscardUpdateErr{Notification: &Notification{Code: 2}}, 577 | ), 578 | }, 579 | want: &Notification{Code: 1}, 580 | }, 581 | { 582 | name: "attr discard", 583 | args: args{ 584 | err: errors.Join( 585 | &AttrDiscardUpdateErr{Notification: &Notification{Code: 1}}, 586 | &fakeUpdateError{Notification: &Notification{Code: 2}}, 587 | ), 588 | }, 589 | want: &Notification{Code: 1}, 590 | }, 591 | { 592 | name: "update error", 593 | args: args{ 594 | err: errors.Join( 595 | errors.New("not a notification"), 596 | &fakeUpdateError{Notification: &Notification{Code: 1}}, 597 | ), 598 | }, 599 | want: &Notification{Code: 1}, 600 | }, 601 | } 602 | for _, tt := range tests { 603 | t.Run(tt.name, func(t *testing.T) { 604 | assert.Equalf(t, tt.want, UpdateNotificationFromErr(tt.args.err), "UpdateNotificationFromErr(%v)", tt.args.err) 605 | }) 606 | } 607 | } 608 | --------------------------------------------------------------------------------