hyprspace/config/config.go
wanderer f1538eef4d
feat: make DNS TLD configurable (#124)
Add a 'domain' option to the NixOS module and JSON config that
controls the domain suffix used for internal DNS names. Defaults to
'hyprspace' for backward compatibility. Supports multi-label domains
(e.g. 'vpn.internal').

Co-authored-by: Max <max@privatevoid.net>
2026-06-22 17:48:14 +00:00

298 lines
7.9 KiB
Go

package config
import (
"encoding/json"
"errors"
"fmt"
"log"
"net"
"os"
"strings"
"github.com/hyprspace/hyprspace/schema"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/multiformats/go-multiaddr"
"github.com/multiformats/go-multibase"
"github.com/yl2chen/cidranger"
)
// Config is the main Configuration Struct for Hyprspace.
type Config struct {
Path string `json:"-"`
Interface string `json:"-"`
ListenAddresses []multiaddr.Multiaddr `json:"-"`
BootstrapPeers []multiaddr.Multiaddr `json:"-"`
Peers []Peer `json:"peers"`
PeerLookup PeerLookup `json:"-"`
PrivateKey crypto.PrivKey `json:"-"`
BuiltinAddr4 net.IP `json:"-"`
BuiltinAddr6 net.IP `json:"-"`
Services map[string]Service `json:"-"`
FilterPrivateAddresses bool `json:"-"`
Domain string `json:"-"`
}
// Peer defines a peer in the configuration. We might add more to this later.
type Peer struct {
ID peer.ID `json:"id"`
Name string `json:"name"`
BuiltinAddr4 net.IP `json:"-"`
BuiltinAddr6 net.IP `json:"-"`
}
// PeerLookup is a helper struct for quickly looking up a peer based on various parameters
type PeerLookup struct {
ByRoute cidranger.Ranger
ByName map[string]Peer
ByNetID map[[4]byte]Peer
}
type RouteTableEntry struct {
Net net.IPNet
Target Peer
}
// Service represents the configuration for a specific service provided by this node.
// Whitelist and Blacklist allow fine granularity in access control.
// If Blacklist is set, this will be evaluated first and any client id present in Blacklist will
// have access denied. Whitelist is evaluated after.
type Service struct {
Target multiaddr.Multiaddr
EnableWhitelist bool
Whitelist map[peer.ID]struct{}
Blacklist map[peer.ID]struct{}
}
func (rte RouteTableEntry) Network() net.IPNet {
return rte.Net
}
// Read initializes a config from a file.
func Read(path string) (*Config, error) {
in, err := os.ReadFile(path)
if err != nil {
return nil, err
}
input := schema.Config{}
result := Config{}
// Read in config settings from file.
err = json.Unmarshal(in, &input)
if err != nil {
return nil, err
}
_, keyBytes, err := multibase.Decode(input.PrivateKey)
if err != nil {
return nil, err
}
pk, err := crypto.UnmarshalPrivateKey(keyBytes)
if err != nil {
return nil, err
}
result.PrivateKey = pk
peerID, err := peer.IDFromPrivateKey(result.PrivateKey)
if err != nil {
return nil, err
}
result.BuiltinAddr4 = mkBuiltinAddr4(peerID)
result.BuiltinAddr6 = mkBuiltinAddr6(peerID)
for _, addrString := range input.ListenAddresses {
addr, err := multiaddr.NewMultiaddr(addrString)
if err != nil {
return nil, err
}
result.ListenAddresses = append(result.ListenAddresses, addr)
}
result.PeerLookup.ByRoute = cidranger.NewPCTrieRanger()
result.PeerLookup.ByName = make(map[string]Peer)
result.PeerLookup.ByNetID = make(map[[4]byte]Peer)
result.Peers = make([]Peer, len(input.Peers))
for i, configPeer := range input.Peers {
p := Peer{}
p.ID, err = peer.Decode(configPeer.Id)
if err != nil {
return nil, err
}
p.Name = configPeer.Name
p.BuiltinAddr4 = mkBuiltinAddr4(p.ID)
p.BuiltinAddr6 = mkBuiltinAddr6(p.ID)
for _, r := range configPeer.Routes {
_, network, err := net.ParseCIDR(r.Net)
if err != nil {
log.Fatal("[!] Invalid network:", r.Net)
}
result.PeerLookup.ByRoute.Insert(&RouteTableEntry{
Net: *network,
Target: p,
})
fmt.Printf("[+] Route %s via /p2p/%s\n", network.String(), p.ID)
}
result.PeerLookup.ByRoute.Insert(&RouteTableEntry{
Net: net.IPNet{
IP: p.BuiltinAddr4,
Mask: net.CIDRMask(32, 32),
},
Target: p,
})
result.PeerLookup.ByRoute.Insert(&RouteTableEntry{
Net: net.IPNet{
IP: p.BuiltinAddr6,
Mask: net.CIDRMask(128, 128),
},
Target: p,
})
if configPeer.Name != "" {
result.PeerLookup.ByName[strings.ToLower(configPeer.Name)] = p
}
result.PeerLookup.ByNetID[[4]byte(p.BuiltinAddr6[12:16])] = p
result.Peers[i] = p
}
result.Services = make(map[string]Service)
for name, service := range input.Services {
addr, err := multiaddr.NewMultiaddr(service.Target)
if err != nil {
return nil, err
}
whitelist := make(map[peer.ID]struct{})
blacklist := make(map[peer.ID]struct{})
for _, p := range service.Acl.Whitelist {
cfgPeer, err := FindPeerByCLIRef(result.Peers, p)
if err != nil {
return nil, err
}
if cfgPeer == nil {
return nil, errors.New("unknown peer: " + p)
}
whitelist[cfgPeer.ID] = struct{}{}
}
for _, peerStr := range service.Acl.Blacklist {
cfgPeer, err := FindPeerByCLIRef(result.Peers, peerStr)
if err != nil {
return nil, err
}
if cfgPeer == nil {
return nil, errors.New("unknown peer: " + peerStr)
}
blacklist[cfgPeer.ID] = struct{}{}
}
result.Services[name] = Service{
Target: addr,
EnableWhitelist: service.Acl.EnableWhitelist,
Whitelist: whitelist,
Blacklist: blacklist,
}
}
result.FilterPrivateAddresses = input.FilterPrivateAddresses
result.Domain = input.Domain
if result.Domain == "" {
result.Domain = "hyprspace"
}
for _, addrString := range input.BootstrapPeers {
addr, err := multiaddr.NewMultiaddr(addrString)
if err != nil {
return nil, err
}
result.BootstrapPeers = append(result.BootstrapPeers, addr)
}
// Overwrite path of config to input.
result.Path = path
return &result, nil
}
func FindPeer(peers []Peer, needle peer.ID) (*Peer, bool) {
for _, p := range peers {
if p.ID == needle {
return &p, true
}
}
return nil, false
}
func FindPeerByName(peers []Peer, needle string) (*Peer, bool) {
for _, p := range peers {
if p.Name == needle {
return &p, true
}
}
return nil, false
}
// FindPeerByIDPrefix returns the unique peer whose ID starts with needle.
// Returns (nil, nil) if no peer matches.
// Returns (nil, error) if more than one peer matches (ambiguous prefix).
func FindPeerByIDPrefix(peers []Peer, needle string) (*Peer, error) {
var match *Peer
count := 0
for i := range peers {
if strings.HasPrefix(peers[i].ID.String(), needle) {
match = &peers[i]
count++
}
}
if count > 1 {
return nil, fmt.Errorf("ambiguous peer prefix %q matches %d peers", needle, count)
}
return match, nil
}
// FindPeerByCLIRef resolves a CLI peer reference: "@name" for name lookup,
// otherwise a peer ID prefix. Returns (nil, nil) on no match,
// (nil, error) only on ambiguous ID prefix.
func FindPeerByCLIRef(peers []Peer, needle string) (*Peer, error) {
if needle == "" {
return nil, nil
}
if strings.HasPrefix(needle, "@") {
name := strings.TrimPrefix(needle, "@")
p, _ := FindPeerByName(peers, name)
return p, nil
}
return FindPeerByIDPrefix(peers, needle)
}
func (cfg Config) FindRoute(needle net.IPNet) (*RouteTableEntry, bool) {
networks, err := cfg.PeerLookup.ByRoute.CoveredNetworks(needle)
if err != nil {
fmt.Println(err)
return nil, false
} else if len(networks) == 0 {
return nil, false
} else if len(networks) > 1 {
for _, n := range networks {
fmt.Printf("[!] Found duplicate route %s to /p2p/%s for %s\n", n.Network(), n.(RouteTableEntry).Target.ID, needle)
}
}
return networks[0].(*RouteTableEntry), true
}
func (cfg Config) FindRouteForIP(needle net.IP) (*RouteTableEntry, bool) {
networks, err := cfg.PeerLookup.ByRoute.ContainingNetworks(needle)
if err != nil {
fmt.Println(err)
return nil, false
} else if len(networks) == 0 {
return nil, false
} else if len(networks) > 1 {
for _, n := range networks {
fmt.Printf("[!] Found duplicate route %s to /p2p/%s for %s\n", n.Network(), n.(RouteTableEntry).Target.ID, needle)
}
}
return networks[0].(*RouteTableEntry), true
}