mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 08:11:32 +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
 | ||
| }
 |