2023-01-27 21:37:20 +00:00
|
|
|
|
// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
2021-08-30 17:45:55 +01:00
|
|
|
|
|
|
|
|
|
// Package chirp implements a client to communicate with the BIRD Internet
|
|
|
|
|
// Routing Daemon.
|
|
|
|
|
package chirp
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bufio"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net"
|
|
|
|
|
"strings"
|
2022-08-28 01:49:31 +01:00
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
// Maximum amount of time we should wait when reading a response from BIRD.
|
|
|
|
|
responseTimeout = 10 * time.Second
|
2021-08-30 17:45:55 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// New creates a BIRDClient.
|
|
|
|
|
func New(socket string) (*BIRDClient, error) {
|
2022-08-28 01:49:31 +01:00
|
|
|
|
return newWithTimeout(socket, responseTimeout)
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-21 05:27:47 +01:00
|
|
|
|
func newWithTimeout(socket string, timeout time.Duration) (_ *BIRDClient, err error) {
|
2021-08-30 17:45:55 +01:00
|
|
|
|
conn, err := net.Dial("unix", socket)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("failed to connect to BIRD: %w", err)
|
|
|
|
|
}
|
2022-09-21 05:27:47 +01:00
|
|
|
|
defer func() {
|
|
|
|
|
if err != nil {
|
|
|
|
|
conn.Close()
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2022-08-28 01:49:31 +01:00
|
|
|
|
b := &BIRDClient{
|
|
|
|
|
socket: socket,
|
|
|
|
|
conn: conn,
|
|
|
|
|
scanner: bufio.NewScanner(conn),
|
|
|
|
|
timeNow: time.Now,
|
|
|
|
|
timeout: timeout,
|
|
|
|
|
}
|
2021-08-30 17:45:55 +01:00
|
|
|
|
// Read and discard the first line as that is the welcome message.
|
2022-02-02 21:05:00 +00:00
|
|
|
|
if _, err := b.readResponse(); err != nil {
|
2021-08-30 17:45:55 +01:00
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return b, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BIRDClient handles communication with the BIRD Internet Routing Daemon.
|
|
|
|
|
type BIRDClient struct {
|
2022-02-02 21:05:00 +00:00
|
|
|
|
socket string
|
|
|
|
|
conn net.Conn
|
|
|
|
|
scanner *bufio.Scanner
|
2022-08-28 01:49:31 +01:00
|
|
|
|
timeNow func() time.Time
|
|
|
|
|
timeout time.Duration
|
2021-08-30 17:45:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close closes the underlying connection to BIRD.
|
|
|
|
|
func (b *BIRDClient) Close() error { return b.conn.Close() }
|
|
|
|
|
|
|
|
|
|
// DisableProtocol disables the provided protocol.
|
|
|
|
|
func (b *BIRDClient) DisableProtocol(protocol string) error {
|
2022-02-02 21:05:00 +00:00
|
|
|
|
out, err := b.exec("disable %s", protocol)
|
2021-08-30 17:45:55 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(out, fmt.Sprintf("%s: already disabled", protocol)) {
|
|
|
|
|
return nil
|
|
|
|
|
} else if strings.Contains(out, fmt.Sprintf("%s: disabled", protocol)) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("failed to disable %s: %v", protocol, out)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// EnableProtocol enables the provided protocol.
|
|
|
|
|
func (b *BIRDClient) EnableProtocol(protocol string) error {
|
2022-02-02 21:05:00 +00:00
|
|
|
|
out, err := b.exec("enable %s", protocol)
|
2021-08-30 17:45:55 +01:00
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(out, fmt.Sprintf("%s: already enabled", protocol)) {
|
|
|
|
|
return nil
|
|
|
|
|
} else if strings.Contains(out, fmt.Sprintf("%s: enabled", protocol)) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return fmt.Errorf("failed to enable %s: %v", protocol, out)
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-02 21:05:00 +00:00
|
|
|
|
// BIRD CLI docs from https://bird.network.cz/?get_doc&v=20&f=prog-2.html#ss2.9
|
|
|
|
|
|
|
|
|
|
// Each session of the CLI consists of a sequence of request and replies,
|
|
|
|
|
// slightly resembling the FTP and SMTP protocols.
|
|
|
|
|
// Requests are commands encoded as a single line of text,
|
|
|
|
|
// replies are sequences of lines starting with a four-digit code
|
|
|
|
|
// followed by either a space (if it's the last line of the reply) or
|
|
|
|
|
// a minus sign (when the reply is going to continue with the next line),
|
|
|
|
|
// the rest of the line contains a textual message semantics of which depends on the numeric code.
|
|
|
|
|
// If a reply line has the same code as the previous one and it's a continuation line,
|
|
|
|
|
// the whole prefix can be replaced by a single white space character.
|
|
|
|
|
//
|
|
|
|
|
// Reply codes starting with 0 stand for ‘action successfully completed’ messages,
|
|
|
|
|
// 1 means ‘table entry’, 8 ‘runtime error’ and 9 ‘syntax error’.
|
|
|
|
|
|
2022-03-16 23:27:57 +00:00
|
|
|
|
func (b *BIRDClient) exec(cmd string, args ...any) (string, error) {
|
2022-08-28 01:49:31 +01:00
|
|
|
|
if err := b.conn.SetWriteDeadline(b.timeNow().Add(b.timeout)); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
2021-08-30 17:45:55 +01:00
|
|
|
|
if _, err := fmt.Fprintf(b.conn, cmd, args...); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
2022-08-28 01:49:31 +01:00
|
|
|
|
if _, err := fmt.Fprintln(b.conn); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
2022-02-02 21:05:00 +00:00
|
|
|
|
return b.readResponse()
|
2021-08-30 17:45:55 +01:00
|
|
|
|
}
|
|
|
|
|
|
2022-02-03 18:25:41 +00:00
|
|
|
|
// hasResponseCode reports whether the provided byte slice is
|
|
|
|
|
// prefixed with a BIRD response code.
|
|
|
|
|
// Equivalent regex: `^\d{4}[ -]`.
|
|
|
|
|
func hasResponseCode(s []byte) bool {
|
|
|
|
|
if len(s) < 5 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, b := range s[:4] {
|
|
|
|
|
if '0' <= b && b <= '9' {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return s[4] == ' ' || s[4] == '-'
|
|
|
|
|
}
|
2022-02-02 21:05:00 +00:00
|
|
|
|
|
|
|
|
|
func (b *BIRDClient) readResponse() (string, error) {
|
2022-08-28 01:49:31 +01:00
|
|
|
|
// Set the read timeout before we start reading anything.
|
|
|
|
|
if err := b.conn.SetReadDeadline(b.timeNow().Add(b.timeout)); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-02 21:05:00 +00:00
|
|
|
|
var resp strings.Builder
|
|
|
|
|
var done bool
|
|
|
|
|
for !done {
|
|
|
|
|
if !b.scanner.Scan() {
|
2022-08-28 01:49:31 +01:00
|
|
|
|
if err := b.scanner.Err(); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "", fmt.Errorf("reading response from bird failed (EOF): %q", resp.String())
|
2022-02-02 21:05:00 +00:00
|
|
|
|
}
|
|
|
|
|
out := b.scanner.Bytes()
|
|
|
|
|
if _, err := resp.Write(out); err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
2022-02-03 18:25:41 +00:00
|
|
|
|
if hasResponseCode(out) {
|
2022-02-02 21:05:00 +00:00
|
|
|
|
done = out[4] == ' '
|
|
|
|
|
}
|
|
|
|
|
if !done {
|
|
|
|
|
resp.WriteRune('\n')
|
|
|
|
|
}
|
2021-08-30 17:45:55 +01:00
|
|
|
|
}
|
2022-02-02 21:05:00 +00:00
|
|
|
|
return resp.String(), nil
|
2021-08-30 17:45:55 +01:00
|
|
|
|
}
|