github.com/letsencrypt/boulder@v0.20251208.0/observer/probers/dns/dns_conf.go (about)

     1  package probers
     2  
     3  import (
     4  	"fmt"
     5  	"net"
     6  	"net/netip"
     7  	"slices"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/miekg/dns"
    12  	"github.com/prometheus/client_golang/prometheus"
    13  
    14  	"github.com/letsencrypt/boulder/observer/probers"
    15  	"github.com/letsencrypt/boulder/strictyaml"
    16  )
    17  
    18  var (
    19  	validQTypes = map[string]uint16{"A": 1, "TXT": 16, "AAAA": 28, "CAA": 257}
    20  )
    21  
    22  // DNSConf is exported to receive YAML configuration
    23  type DNSConf struct {
    24  	Proto   string `yaml:"protocol"`
    25  	Server  string `yaml:"server"`
    26  	Recurse bool   `yaml:"recurse"`
    27  	QName   string `yaml:"query_name"`
    28  	QType   string `yaml:"query_type"`
    29  }
    30  
    31  // Kind returns a name that uniquely identifies the `Kind` of `Configurer`.
    32  func (c DNSConf) Kind() string {
    33  	return "DNS"
    34  }
    35  
    36  // UnmarshalSettings constructs a DNSConf object from YAML as bytes.
    37  func (c DNSConf) UnmarshalSettings(settings []byte) (probers.Configurer, error) {
    38  	var conf DNSConf
    39  	err := strictyaml.Unmarshal(settings, &conf)
    40  	if err != nil {
    41  		return nil, err
    42  	}
    43  	return conf, nil
    44  }
    45  
    46  func (c DNSConf) validateServer() error {
    47  	server := strings.Trim(strings.ToLower(c.Server), " ")
    48  	// Ensure `server` contains a port.
    49  	host, port, err := net.SplitHostPort(server)
    50  	if err != nil || port == "" {
    51  		return fmt.Errorf(
    52  			"invalid `server`, %q, could not be split: %s", c.Server, err)
    53  	}
    54  	// Ensure `server` port is valid.
    55  	portNum, err := strconv.Atoi(port)
    56  	if err != nil {
    57  		return fmt.Errorf(
    58  			"invalid `server`, %q, port must be a number", c.Server)
    59  	}
    60  	if portNum <= 0 || portNum > 65535 {
    61  		return fmt.Errorf(
    62  			"invalid `server`, %q, port number must be one in [1-65535]", c.Server)
    63  	}
    64  	// Ensure `server` is a valid FQDN or IP address.
    65  	_, err = netip.ParseAddr(host)
    66  	FQDN := dns.IsFqdn(dns.Fqdn(host))
    67  	if err != nil && !FQDN {
    68  		return fmt.Errorf(
    69  			"invalid `server`, %q, is not an FQDN or IP address", c.Server)
    70  	}
    71  	return nil
    72  }
    73  
    74  func (c DNSConf) validateProto() error {
    75  	validProtos := []string{"udp", "tcp"}
    76  	proto := strings.Trim(strings.ToLower(c.Proto), " ")
    77  	if slices.Contains(validProtos, proto) {
    78  		return nil
    79  	}
    80  	return fmt.Errorf(
    81  		"invalid `protocol`, got: %q, expected one in: %s", c.Proto, validProtos)
    82  }
    83  
    84  func (c DNSConf) validateQType() error {
    85  	validQTypes = map[string]uint16{"A": 1, "TXT": 16, "AAAA": 28, "CAA": 257}
    86  	qtype := strings.Trim(strings.ToUpper(c.QType), " ")
    87  	q := make([]string, 0, len(validQTypes))
    88  	for i := range validQTypes {
    89  		q = append(q, i)
    90  		if qtype == i {
    91  			return nil
    92  		}
    93  	}
    94  	return fmt.Errorf(
    95  		"invalid `query_type`, got: %q, expected one in %s", c.QType, q)
    96  }
    97  
    98  // MakeProber constructs a `DNSProbe` object from the contents of the
    99  // bound `DNSConf` object. If the `DNSConf` cannot be validated, an
   100  // error appropriate for end-user consumption is returned instead.
   101  func (c DNSConf) MakeProber(_ map[string]prometheus.Collector) (probers.Prober, error) {
   102  	// validate `query_name`
   103  	if !dns.IsFqdn(dns.Fqdn(c.QName)) {
   104  		return nil, fmt.Errorf(
   105  			"invalid `query_name`, %q is not an fqdn", c.QName)
   106  	}
   107  
   108  	// validate `server`
   109  	err := c.validateServer()
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  
   114  	// validate `protocol`
   115  	err = c.validateProto()
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  
   120  	// validate `query_type`
   121  	err = c.validateQType()
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	return DNSProbe{
   127  		proto:   strings.Trim(strings.ToLower(c.Proto), " "),
   128  		recurse: c.Recurse,
   129  		qname:   c.QName,
   130  		server:  c.Server,
   131  		qtype:   validQTypes[strings.Trim(strings.ToUpper(c.QType), " ")],
   132  	}, nil
   133  }
   134  
   135  // Instrument is a no-op to implement the `Configurer` interface.
   136  func (c DNSConf) Instrument() map[string]prometheus.Collector {
   137  	return nil
   138  }
   139  
   140  // init is called at runtime and registers `DNSConf`, a `Prober`
   141  // `Configurer` type, as "DNS".
   142  func init() {
   143  	probers.Register(DNSConf{})
   144  }