Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions agent/agent_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package agent
import (
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"strings"
Expand Down Expand Up @@ -121,6 +122,7 @@ func (s *HTTPHandlers) AgentSelf(resp http.ResponseWriter, req *http.Request) (i
Server bool
Version string
BuildDate string
BindAddr net.IP
}{
Datacenter: displayConfig.Datacenter,
PrimaryDatacenter: displayConfig.PrimaryDatacenter,
Expand All @@ -132,6 +134,12 @@ func (s *HTTPHandlers) AgentSelf(resp http.ResponseWriter, req *http.Request) (i
// We expect the ent version to be part of the reported version string, and that's now part of the metadata, not the actual version.
Version: displayConfig.VersionWithMetadata(),
BuildDate: displayConfig.BuildDate.Format(time.RFC3339),
BindAddr: func() net.IP {
if displayConfig.BindAddr != nil {
return displayConfig.BindAddr.IP
}
return nil
}(),
}

return Self{
Expand Down
99 changes: 93 additions & 6 deletions agent/consul/state/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package state
import (
"errors"
"fmt"
"github.com/hashicorp/consul/agent/netutil"
"github.com/hashicorp/consul/api"
"net"
"reflect"
"slices"
Expand All @@ -17,7 +19,6 @@ import (
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/configentry"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/lib/maps"
"github.com/hashicorp/consul/lib/stringslice"
Expand Down Expand Up @@ -48,6 +49,20 @@ var (

virtualIPMaxOffset = net.IP{15, 255, 255, 254}

startingVirtualIPv6 = net.IP{
0x20, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
}

virtualIPv6MaxOffset = net.IP{
0x1F, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF,
}

ErrNodeNotFound = errors.New("node not found")
)

Expand Down Expand Up @@ -1009,7 +1024,7 @@ func assignServiceVirtualIP(tx WriteTxn, idx uint64, psn structs.PeeredServiceNa
// Service already has a virtual IP assigned, nothing to do.
if serviceVIP != nil {
sVIP := serviceVIP.(ServiceVirtualIP).IP
result, err := addIPOffset(startingVirtualIP, sVIP)
result, err := addIPOffset(sVIP)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -1060,9 +1075,12 @@ func assignServiceVirtualIP(tx WriteTxn, idx uint64, psn structs.PeeredServiceNa
break
}
}

maxIPOffset := virtualIPMaxOffset
if p := net.ParseIP(newEntry.IP.String()); p == nil || p.To4() == nil {
maxIPOffset = virtualIPv6MaxOffset
}
// Out of virtual IPs, fail registration.
if newEntry.IP.Equal(virtualIPMaxOffset) {
if newEntry.IP.Equal(maxIPOffset) {
return "", fmt.Errorf("cannot allocate any more unique service virtual IPs")
}

Expand All @@ -1086,7 +1104,7 @@ func assignServiceVirtualIP(tx WriteTxn, idx uint64, psn structs.PeeredServiceNa
return "", err
}

result, err := addIPOffset(startingVirtualIP, assignedVIP.IP)
result, err := addIPOffset(assignedVIP.IP)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -1212,7 +1230,76 @@ func updateVirtualIPMaxIndexes(txn WriteTxn, idx uint64, partition, peerName str
return nil
}

func addIPOffset(a, b net.IP) (net.IP, error) {
func addIPOffset(ip net.IP) (net.IP, error) {
bindAddr, err := netutil.GetAgentBindAddr()
if err != nil {
return nil, err
}

var vip net.IP
switch {
case bindAddr.To4() == nil: // IPv6
vip, err = addIPv6Offset(startingVirtualIPv6, ip)
case bindAddr.To16() == nil: // IPv4
vip, err = addIPv4Offset(startingVirtualIP, ip)
default:
err = fmt.Errorf("invalid bind address: %v", bindAddr)
}

return vip, err
}

func getAgentBindAddr() (net.IP, error) {
agentConfig, err := netutil.GetAgentConfig()
if err != nil {
return nil, err
}

configMap, ok := agentConfig["Config"]
if !ok {
return nil, errors.New("agent config 'Config' field is not a map")
}

bindAddr, ok := configMap["BindAddr"]
if !ok {
return nil, errors.New("BindAddr is not set in agent config")
}

ip, ok := bindAddr.(net.IP)
if !ok {
return nil, errors.New("BindAddr is not of type net.IP")
}

return ip, nil
}

// addIPv6Offset adds two IPv6 address byte slices (a and b).
// Both must be 16 bytes long.
// Returns the sum modulo 2^128 as a new IPv6 address.
func addIPv6Offset(a, b net.IP) (net.IP, error) {
a16 := a.To16()
b16 := b.To16()
if a16 == nil || b16 == nil {
return nil, errors.New("ip is not valid IPv6")
}
if len(a16) != 16 || len(b16) != 16 {
return nil, errors.New("ip length is not 16 bytes")
}

result := make(net.IP, 16)
var carry uint16 = 0

// Add from the least significant byte to most significant byte
for i := 15; i >= 0; i-- {
sum := uint16(a16[i]) + uint16(b16[i]) + carry
result[i] = byte(sum & 0xFF)
carry = sum >> 8 // carry 1 if sum > 255
}
// Carry beyond 128 bits is discarded (mod 2^128 arithmetic)
return result, nil
}

func addIPv4Offset(a, b net.IP) (net.IP, error) {
a4 := a.To4()
b4 := b.To4()
if a4 == nil || b4 == nil {
Expand Down
4 changes: 3 additions & 1 deletion agent/consul/state/catalog_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,9 @@ type ServiceVirtualIP struct {
}

func (s ServiceVirtualIP) IPWithOffset() (string, error) {
result, err := addIPOffset(startingVirtualIP, s.IP)
var result net.IP
var err error
result, err = addIPOffset(s.IP)
if err != nil {
return "", err
}
Expand Down
48 changes: 36 additions & 12 deletions agent/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,7 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, cfg *dnsRe
// Get the QName without the domain suffix
qName := strings.ToLower(dns.Fqdn(req.Question[0].Name))
qName = d.trimDomain(qName)
qType := req.Question[0].Qtype

// Split into the label parts
labels := dns.SplitDomainName(qName)
Expand All @@ -791,7 +792,7 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, cfg *dnsRe
default:
// If this is a SRV query the "service" label is optional, we add it back to use the
// existing code-path.
if req.Question[0].Qtype == dns.TypeSRV && strings.HasPrefix(labels[i], "_") {
if qType == dns.TypeSRV && strings.HasPrefix(labels[i], "_") {
queryKind = "service"
queryParts = labels[:i+1]
querySuffixes = labels[i+1:]
Expand Down Expand Up @@ -921,15 +922,38 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, cfg *dnsRe
return err
}
if out != "" {
resp.Answer = append(resp.Answer, &dns.A{
Hdr: dns.RR_Header{
Name: qName + respDomain,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: uint32(cfg.NodeTTL / time.Second),
},
A: net.ParseIP(out),
})
p := net.ParseIP(out)
if p.To4() == nil {
aaaaRecord := &dns.AAAA{
Hdr: dns.RR_Header{
Name: qName + respDomain,
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET,
Ttl: uint32(cfg.NodeTTL / time.Second),
},
AAAA: p,
}
if qType != dns.TypeAAAA && qType != dns.TypeANY {
resp.Extra = append(resp.Extra, aaaaRecord)
} else {
resp.Answer = append(resp.Answer, aaaaRecord)
}
} else {
aRecord := &dns.A{
Hdr: dns.RR_Header{
Name: qName + respDomain,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: uint32(cfg.NodeTTL / time.Second),
},
A: p,
}
if qType != dns.TypeA && qType != dns.TypeANY {
resp.Extra = append(resp.Answer, aRecord)
} else {
resp.Answer = append(resp.Answer, aRecord)
}
}
}

return nil
Expand Down Expand Up @@ -1047,7 +1071,7 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, cfg *dnsRe
},
A: ip,
}
if req.Question[0].Qtype != dns.TypeA && req.Question[0].Qtype != dns.TypeANY {
if qType != dns.TypeA && qType != dns.TypeANY {
resp.Extra = append(resp.Answer, aRecord)
} else {
resp.Answer = append(resp.Answer, aRecord)
Expand All @@ -1068,7 +1092,7 @@ func (d *DNSServer) dispatch(remoteAddr net.Addr, req, resp *dns.Msg, cfg *dnsRe
},
AAAA: ip,
}
if req.Question[0].Qtype != dns.TypeAAAA && req.Question[0].Qtype != dns.TypeANY {
if qType != dns.TypeAAAA && qType != dns.TypeANY {
resp.Extra = append(resp.Extra, aaaaRecord)
} else {
resp.Answer = append(resp.Answer, aaaaRecord)
Expand Down
75 changes: 75 additions & 0 deletions agent/netutil/network.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package netutil

import (
"net"
"net/netip"

"github.com/hashicorp/consul/api"
)

// GetAgentConfigFunc is the function type for getting agent config
var GetAgentConfigFunc = GetAgentConfig

var GetAgentBindAddrFunc = GetAgentBindAddr

// GetAgentConfig retrieves the agent's configuration using the local Consul agent's API.
func GetAgentConfig() (map[string]map[string]interface{}, error) {
client, err := api.NewClient(api.DefaultConfig())
if err != nil {
return nil, err
}

self, err := client.Agent().Self()
if err != nil {
return nil, err
}

return self, nil
}

// GetAgentBindAddr retrieves the bind address from the agent's configuration.
func GetAgentBindAddr() (net.IP, error) {
agentConfig, err := GetAgentConfigFunc()
if err != nil {
return nil, err
}

bindAddr, ok := agentConfig["Config"]["BindAddr"].(string)
if !ok || bindAddr == "" {
return nil, nil
}

ip, err := netip.ParseAddr(bindAddr)
if err != nil {
return nil, err
}

return ip.AsSlice(), nil
}

// IsDualStack checks if the agent is configured to use both IPv4 and IPv6 addresses.
// It returns true if the agent is running in dual-stack mode, false otherwise.
// An error is returned if the agent's bind address cannot be determined.
func IsDualStack() (bool, error) {
bindIP, err := GetAgentBindAddrFunc()
if err != nil {
return false, err
}

// If no bind address is set, assume dual-stack is not enabled
if bindIP == nil {
return false, nil
}

// Check if the bind address is an IPv4-mapped IPv6 address
if bindIP.To4() != nil {
// IPv4 address
return false, nil
}

// For IPv6, check if it's a dual-stack address
return bindIP.To16() != nil && bindIP.To4() == nil, nil
}
Loading
Loading