You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailscale/chirp/chirp.go

164 lines
4.4 KiB
Go

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package chirp implements a client to communicate with the BIRD Internet
// Routing Daemon.
package chirp
import (
"bufio"
"fmt"
"net"
"strings"
"time"
)
const (
// Maximum amount of time we should wait when reading a response from BIRD.
responseTimeout = 10 * time.Second
)
// New creates a BIRDClient.
func New(socket string) (*BIRDClient, error) {
return newWithTimeout(socket, responseTimeout)
}
func newWithTimeout(socket string, timeout time.Duration) (_ *BIRDClient, err error) {
conn, err := net.Dial("unix", socket)
if err != nil {
return nil, fmt.Errorf("failed to connect to BIRD: %w", err)
}
defer func() {
if err != nil {
conn.Close()
}
}()
b := &BIRDClient{
socket: socket,
conn: conn,
scanner: bufio.NewScanner(conn),
timeNow: time.Now,
timeout: timeout,
}
// Read and discard the first line as that is the welcome message.
if _, err := b.readResponse(); err != nil {
return nil, err
}
return b, nil
}
// BIRDClient handles communication with the BIRD Internet Routing Daemon.
type BIRDClient struct {
socket string
conn net.Conn
scanner *bufio.Scanner
timeNow func() time.Time
timeout time.Duration
}
// 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 {
out, err := b.exec("disable %s", protocol)
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 {
out, err := b.exec("enable %s", protocol)
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)
}
// 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.
func (b *BIRDClient) exec(cmd string, args ...any) (string, error) {
if err := b.conn.SetWriteDeadline(b.timeNow().Add(b.timeout)); err != nil {
return "", err
}
if _, err := fmt.Fprintf(b.conn, cmd, args...); err != nil {
return "", err
}
if _, err := fmt.Fprintln(b.conn); err != nil {
return "", err
}
return b.readResponse()
}
// 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] == '-'
}
func (b *BIRDClient) readResponse() (string, error) {
// Set the read timeout before we start reading anything.
if err := b.conn.SetReadDeadline(b.timeNow().Add(b.timeout)); err != nil {
return "", err
}
var resp strings.Builder
var done bool
for !done {
if !b.scanner.Scan() {
if err := b.scanner.Err(); err != nil {
return "", err
}
return "", fmt.Errorf("reading response from bird failed (EOF): %q", resp.String())
}
out := b.scanner.Bytes()
if _, err := resp.Write(out); err != nil {
return "", err
}
if hasResponseCode(out) {
done = out[4] == ' '
}
if !done {
resp.WriteRune('\n')
}
}
return resp.String(), nil
}