mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-11-04 10:11:18 +01:00 
			
		
		
		
	This updates all source files to use a new standard header for copyright and license declaration. Notably, copyright no longer includes a date, and we now use the standard SPDX-License-Identifier header. This commit was done almost entirely mechanically with perl, and then some minimal manual fixes. Updates #6865 Signed-off-by: Will Norris <will@tailscale.com>
		
			
				
	
	
		
			164 lines
		
	
	
		
			4.4 KiB
		
	
	
	
		
			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
 | 
						||
}
 |