commit fec65603f5fbe492720d0a50007a6257fb6922d8 Author: Kleissner Date: Fri Feb 26 05:57:42 2021 +0100 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3fbf3ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.exe +log.txt +*debug_bin +*.yaml diff --git a/Bootstrap.go b/Bootstrap.go new file mode 100644 index 0000000..bb722b3 --- /dev/null +++ b/Bootstrap.go @@ -0,0 +1,187 @@ +/* +File Name: Bootstrap.go +Copyright: 2021 Peernet Foundation s.r.o. +Author: Peter Kleissner + +Strategy for sending our IPv6 Multicast and IPv4 Broadcast messages: +* During bootstrap: Immediately at the beginning, then every 10 seconds until there is at least 1 peer. +* Every 10 minutes during regular operation. +* Each time a network adapter / IP change is detected. [FUTURE] + +*/ + +package core + +import ( + "encoding/hex" + "errors" + "log" + "net" + "strconv" + "time" + + "github.com/btcsuite/btcd/btcec" +) + +// rootPeer is a single root peer info +type rootPeer struct { + peer *PeerInfo // loaded PeerInfo + publicKey *btcec.PublicKey // Public key + addresses []*net.UDPAddr // IP:Port addresses +} + +var rootPeers map[[btcec.PubKeyBytesLenCompressed]byte]*rootPeer + +// initSeedList loads the seed list from the config +func initSeedList() { + rootPeers = make(map[[btcec.PubKeyBytesLenCompressed]byte]*rootPeer) + +loopSeedList: + for _, seed := range config.SeedList { + peer := &rootPeer{} + + // parse the Public Key + publicKeyB, err := hex.DecodeString(seed.PublicKey) + if err != nil { + log.Printf("initSeedList error public key '%s': %v", seed.PublicKey, err.Error()) + continue + } + + if peer.publicKey, err = btcec.ParsePubKey(publicKeyB, btcec.S256()); err != nil { + log.Printf("initSeedList error public key '%s': %v", seed.PublicKey, err.Error()) + continue + } + + // parse all IP addresses + for _, addressA := range seed.Address { + address, err := parseAddress(addressA) + if err != nil { + log.Printf("initSeedList error public key '%s' address '%s': %v", seed.PublicKey, addressA, err.Error()) + continue loopSeedList + } + + peer.addresses = append(peer.addresses, address) + } + + rootPeers[publicKey2Compressed(peer.publicKey)] = peer + } +} + +// parseAddress parses an input peer address in the form "IP:Port". +func parseAddress(Address string) (remote *net.UDPAddr, err error) { + host, portA, err := net.SplitHostPort(Address) + if err != nil { + return nil, err + } + + portI, err := strconv.Atoi(portA) + if err != nil { + return nil, err + } else if portI <= 0 || portI > 65535 { + return nil, errors.New("invalid port number") + } + + ip := net.ParseIP(host) + if ip == nil { + return nil, errors.New("invalid input IP") + } + + return &net.UDPAddr{IP: ip, Port: portI}, err +} + +// contact tries to contact the root peer on all networks +func (peer *rootPeer) contact() { + for _, address := range peer.addresses { + sendAllNetworks(peer.publicKey, &packetRaw{Command: 0}, address) + } +} + +// bootstrap connects to the initial set of peers. It will also start the routine for ongoing sending of multicast/broadcast messages. +func bootstrap() { + if len(rootPeers) == 0 { + log.Printf("bootstrap warning: Empty list of root peers. Connectivity relies on local peer discovery and incoming connections.\n") + return + } + + contactRootPeers := func() { + for _, peer := range rootPeers { + if peer.peer == nil { + peer.contact() + } + } + } + + countConnectedRootPeers := func() (connectedCount, total int) { + for _, peer := range rootPeers { + if peer.peer != nil { + connectedCount++ + } else if peer.peer = PeerlistLookup(peer.publicKey); peer.peer != nil { + connectedCount++ + } + } + return connectedCount, len(rootPeers) + } + + // initial contact to all root peer + contactRootPeers() + + // Phase 1: First 10 minutes. Try every 7 seconds to connect to all root peers until at least 2 peers connected. + for n := 0; n < 10*60/7; n++ { + time.Sleep(time.Second * 7) + + if connected, total := countConnectedRootPeers(); connected == total || connected >= 2 { + return + } + + contactRootPeers() + } + + // Phase 2: After that (if not 2 peers), try every 5 minutes to connect to remaining root peers for a maximum of 1 hour. + for n := 0; n < 1*60/5; n++ { + time.Sleep(time.Minute * 5) + + contactRootPeers() + + if connected, total := countConnectedRootPeers(); connected == total || connected >= 2 { + return + } + } + + log.Printf("bootstrap unable to connect to at least 2 root peers, aborting\n") +} + +func autoMulticastBroadcast() { + sendMulticastBroadcast := func() { + for _, network := range networks6 { + if err := network.MulticastIPv6Send(); err != nil { + log.Printf("bootstrap error multicast from network address '%s': %v", network.address.IP.String(), err.Error()) + } + } + + for _, network := range networks4 { + if err := network.BroadcastIPv4Send(); err != nil { + log.Printf("bootstrap error broadcast from network address '%s': %v", network.address.IP.String(), err.Error()) + } + } + } + + // Send out multicast/broadcast immediately. + sendMulticastBroadcast() + + // Phase 1: Resend every 10 seconds until at least 1 peer in the peer list + for { + time.Sleep(time.Second * 10) + + if PeerlistCount() >= 1 { + break + } + + sendMulticastBroadcast() + } + + // Phase 2: Every 10 minutes. + for { + time.Sleep(time.Minute * 10) + sendMulticastBroadcast() + } +} diff --git a/Commands.go b/Commands.go new file mode 100644 index 0000000..af71201 --- /dev/null +++ b/Commands.go @@ -0,0 +1,73 @@ +/* +File Name: Commands.go +Copyright: 2021 Peernet Foundation s.r.o. +Author: Peter Kleissner +*/ + +package core + +import ( + "fmt" + "net" + + "github.com/btcsuite/btcd/btcec" +) + +// packet2 is a high-level message between peers +type packet2 struct { + packetRaw + SenderPublicKey *btcec.PublicKey // Sender Public Key, ECDSA (secp256k1) 257-bit + network *Network // network which received the packet + address *net.UDPAddr // address of the sender or receiver +} + +// announcement handles an incoming announcement +func (peer *PeerInfo) announcement(msg *packet2) { + if peer == nil { + peer, added := PeerlistAdd(msg.SenderPublicKey, &Connection{Network: msg.network, Address: msg.address}) + fmt.Printf("Incoming initial announcement from %s\n", msg.address.String()) + + // send the Response + if added { + peer.send(&packetRaw{Command: 1}) + } + + return + } + fmt.Printf("Incoming secondary announcement from %s\n", msg.address.String()) + + // Announcement from existing peer means the peer most likely restarted + peer.send(&packetRaw{Command: 1}) +} + +// response handles the response to the announcement +func (peer *PeerInfo) response(msg *packet2) { + if peer == nil { + peer, _ = PeerlistAdd(msg.SenderPublicKey, &Connection{Network: msg.network, Address: msg.address}) + fmt.Printf("Incoming initial response from %s\n", msg.address.String()) + + return + } + + fmt.Printf("Incoming response from %s on %s\n", msg.address.String(), msg.network.address.String()) +} + +// chat handles a chat message [debug] +func (peer *PeerInfo) chat(msg *packet2) { + fmt.Printf("Chat from '%s': %s\n", msg.address.String(), string(msg.packetRaw.Payload)) +} + +// autoPingAll automatically pings all peers. This has multiple important reasons: +// * Keeping the UDP port open at NAT, if applicable +// * Making sure to recognize invalid connections and drop them. +func autoPingAll() { + + // TODO: If peer was previously unresponsive for X times, start pinging on all available ports. +} + +// SendChatAll sends a text message to all peers +func SendChatAll(text string) { + for _, peer := range PeerlistGet() { + peer.send(&packetRaw{Command: 10, Payload: []byte(text)}) + } +} diff --git a/Network Detection.go b/Network Detection.go new file mode 100644 index 0000000..0ad6ec9 --- /dev/null +++ b/Network Detection.go @@ -0,0 +1,71 @@ +/* +File Name: Network Detection.go +Copyright: 2021 Peernet Foundation s.r.o. +Author: Peter Kleissner +*/ + +package core + +import "net" + +// FindInterfaceByIP finds an interface based on the IP. The IP must be available at the interface. +func FindInterfaceByIP(ip net.IP) (iface *net.Interface, ipnet *net.IPNet) { + interfaceList, err := net.Interfaces() + if err != nil { + return nil, nil + } + + // iterate through all interfaces + for _, ifaceSingle := range interfaceList { + addresses, err := ifaceSingle.Addrs() + if err != nil { + continue + } + + // iterate through all IPs of the interfaces + for _, address := range addresses { + addressIP := address.(*net.IPNet).IP + + if addressIP.Equal(ip) { + return &ifaceSingle, address.(*net.IPNet) + } + } + } + + return nil, nil +} + +// NetworkListIPs returns a list of all IPs +func NetworkListIPs() (IPs []net.IP, err error) { + + interfaceList, err := net.Interfaces() + if err != nil { + return nil, err + } + + // iterate through all interfaces + for _, ifaceSingle := range interfaceList { + addresses, err := ifaceSingle.Addrs() + if err != nil { + continue + } + + // iterate through all IPs of the interfaces + for _, address := range addresses { + addressIP := address.(*net.IPNet).IP + IPs = append(IPs, addressIP) + } + } + + return IPs, nil +} + +// IsIPv4 checks if an IP address is IPv4 +func IsIPv4(IP net.IP) bool { + return IP.To4() != nil +} + +// IsIPv6 checks if an IP address is IPv6 +func IsIPv6(IP net.IP) bool { + return IP.To4() == nil && IP.To16() != nil +} diff --git a/Network IPv4 Broadcast.go b/Network IPv4 Broadcast.go new file mode 100644 index 0000000..a6641fc --- /dev/null +++ b/Network IPv4 Broadcast.go @@ -0,0 +1,168 @@ +/* +File Name: Network IPv4 Broadcast.go +Copyright: 2021 Peernet Foundation s.r.o. +Author: Peter Kleissner + +IPv4 Multicast just sucks (can't use socket bound to 0.0.0.0:PortMain and send to 224.0.0.1:PortMulticast), so we rely on Broadcast instead. +*/ + +package core + +import ( + "encoding/hex" + "log" + "net" + "strconv" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/libp2p/go-reuseport" +) + +const ipv4BroadcastPort = 12912 + +// special Public-Private Key pair for local discovery +var ipv4BroadcastPrivateKey *btcec.PrivateKey +var ipv4BroadcastPublicKey *btcec.PublicKey + +const ipv4BroadcastPrivateKeyH = "5e27ecc8e54a24e71dca9ba84a9bf465400e27b8c46a977d34962d3d88558c8e" + +func initBroadcastIPv4() { + if configPK, err := hex.DecodeString(ipv4BroadcastPrivateKeyH); err == nil { + ipv4BroadcastPrivateKey, ipv4BroadcastPublicKey = btcec.PrivKeyFromBytes(btcec.S256(), configPK) + } +} + +// BroadcastIPv4Join prepares sending Broadcasts +func (network *Network) BroadcastIPv4() (err error) { + if ipv4BroadcastPrivateKey == nil || ipv4BroadcastPublicKey == nil { + return + } + + // listen on a special socket + network.broadcastSocket, err = reuseport.ListenPacket("udp4", net.JoinHostPort(network.address.IP.String(), strconv.Itoa(ipv4BroadcastPort))) + if err != nil { + return err + } + + network.broadcastIPv4 = networkToIPv4BroadcastIPs(network.ipnet) + + go network.BroadcastIPv4Listen() + + return nil +} + +// BroadcastIPv4Listen listens for incoming broadcast packets +// Fork from network.Listen! Keep any changes synced. +func (network *Network) BroadcastIPv4Listen() { + for { + // Buffer: Must be created for each packet as it is passed as pointer. + // If the buffer is too small, ReadFromUDP only reads until its length and returns this error: "wsarecvfrom: A message sent on a datagram socket was larger than the internal message buffer or some other network limit, or the buffer used to receive a datagram into was smaller than the datagram itself." + buffer := make([]byte, maxPacketSize) + length, sender, err := network.broadcastSocket.ReadFrom(buffer) + + if err != nil { + log.Printf("Listen Error receiving UDP message: %v\n", err) // Only log for debug purposes. + time.Sleep(time.Millisecond * 50) // In case of endless errors, prevent ddos of CPU. + continue + } + + if IsAddressSelf(sender.(*net.UDPAddr)) { + continue + } + + //fmt.Printf("BroadcastIPv4Listen from %s at network %s\n", sender.String(), network.address.String()) + + if length < packetLengthMin { + // Discard packets that do not meet the minimum length. + continue + } + + // send the packet to a channel which is processed by multiple workers. + rawPacketsIncoming <- networkWire{network: network, sender: sender.(*net.UDPAddr), raw: buffer[:length], receiverPublicKey: ipv4BroadcastPublicKey} + } +} + +// BroadcastIPv4Send sends out a single broadcast messages to discover peers +func (network *Network) BroadcastIPv4Send() (err error) { + raw, err := packetEncrypt(peerPrivateKey, ipv4BroadcastPublicKey, &packetRaw{Protocol: 0, Command: 0}) + if err != nil { + return err + } + + // send out the wire + for _, ip := range network.broadcastIPv4 { + err = network.send(ip, ipv4BroadcastPort, raw) + if err != nil { + log.Printf("Error sending UDP packet: %v\n", err) + } + } + + return nil +} + +// networkToIPv4BroadcastIPs generates the IPv4 addresses to send out the broadcast to +func networkToIPv4BroadcastIPs(ipnet *net.IPNet) (broadcastIPs []net.IP) { + broadcastIPs = append(broadcastIPs, net.IPv4bcast) + + if ipnet != nil { + if ip2 := ipv4DirectedBroadcast(ipnet); ip2 != nil { + broadcastIPs = append(broadcastIPs, ip2) + } + } else { + interfaceList, err := net.Interfaces() + if err != nil { + return + } + + for _, iface := range interfaceList { + addresses, err := iface.Addrs() + if err != nil { + continue + } + + for _, address := range addresses { + net1 := address.(*net.IPNet) + + // TODO: Does the rfc3927Net make sense? + if !IsIPv4(net1.IP) || rfc3927Net.Contains(net1.IP) { + continue + } + + if ip2 := ipv4DirectedBroadcast(net1); ip2 != nil { + broadcastIPs = append(broadcastIPs, ip2) + } + } + } + } + + // TODO: Result could contain duplicates, filter them out + + return broadcastIPs +} + +func ipv4DirectedBroadcast(n *net.IPNet) net.IP { + ip4 := n.IP.To4() + if ip4 == nil { + return nil + } + last := make(net.IP, len(ip4)) + copy(last, ip4) + for i := range ip4 { + last[i] |= ^n.Mask[i] + } + return last +} + +var ( + // rfc3927Net specifies the IPv4 auto configuration address block as + // defined by RFC3927 (169.254.0.0/16). + rfc3927Net = ipNet("169.254.0.0", 16, 32) +) + +// ipNet returns a net.IPNet struct given the passed IP address string, number +// of one bits to include at the start of the mask, and the total number of bits +// for the mask. +func ipNet(ip string, ones, bits int) net.IPNet { + return net.IPNet{IP: net.ParseIP(ip), Mask: net.CIDRMask(ones, bits)} +} diff --git a/Network IPv6 Multicast.go b/Network IPv6 Multicast.go new file mode 100644 index 0000000..f8b3980 --- /dev/null +++ b/Network IPv6 Multicast.go @@ -0,0 +1,141 @@ +/* +File Name: Network IPv6 Multicast.go +Copyright: 2021 Peernet Foundation s.r.o. +Author: Peter Kleissner + +IPv6 Multicast implementation to support discovery of peers within the same network (Site-local). +Loopback is enabled, which means that Multicast packets sent will be looped back and received by any local listeners. This allows to connect local processes with each other. + +Using the separate Multicast port, it allows sending unsolicited announcements without knowing the target's public key. Instead, a hard-coded key is used. + +The Multicast listener opens port 12912 with SO_REUSEADDR to allow multiple processes receive the incoming Multicast packets. +[1] mentions "If two sockets are bound to the same interface and port and are members of the same multicast group, data will be delivered to both sockets, rather than an arbitrarily chosen one." + +[1] https://docs.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse +*/ + +package core + +import ( + "encoding/hex" + "log" + "net" + "strconv" + "time" + + "github.com/btcsuite/btcd/btcec" + "github.com/libp2p/go-reuseport" + "golang.org/x/net/ipv6" +) + +// Multicast group is site-local. Group ID is 112. +const ipv6MulticastGroup = "ff05::112" +const ipv6MulticastPort = 12912 + +// special Public-Private Key pair for local discovery +var ipv6MulticastPrivateKey *btcec.PrivateKey +var ipv6MulticastPublicKey *btcec.PublicKey + +const ipv6MulticastPrivateKeyH = "016ad30bfb369926523bf18d136298b6c31d0817e9fb6c21feed89ae22cad788" + +func initMulticastIPv6() { + if configPK, err := hex.DecodeString(ipv6MulticastPrivateKeyH); err == nil { + ipv6MulticastPrivateKey, ipv6MulticastPublicKey = btcec.PrivKeyFromBytes(btcec.S256(), configPK) + } +} + +// MulticastIPv6Join joins the Multicast group +func (network *Network) MulticastIPv6Join() (err error) { + if ipv6MulticastPrivateKey == nil || ipv6MulticastPublicKey == nil { + return + } + + network.multicastIP = net.ParseIP(ipv6MulticastGroup) + + // listen on a special socket + network.multicastSocket, err = reuseport.ListenPacket("udp6", net.JoinHostPort(network.address.IP.String(), strconv.Itoa(ipv6MulticastPort))) + if err != nil { + return err + } + + joinMulticastGroup := func(iface *net.Interface) (err error) { + pc := ipv6.NewPacketConn(network.multicastSocket) + if err := pc.JoinGroup(iface, &net.UDPAddr{IP: network.multicastIP}); err != nil { + return err + } + + // receive messages from self or other processes running on the same computer + if loop, err := pc.MulticastLoopback(); err == nil { + if !loop { + if err := pc.SetMulticastLoopback(true); err != nil { + log.Printf("MulticastJoin Error setting multicast loopback status: %v\n", err) + } + } + } + + return nil + } + + // specific interface or join all? + if network.iface != nil { + if err = joinMulticastGroup(network.iface); err != nil { + return err + } + } else { + interfaceList, err := net.Interfaces() + if err != nil { + return err + } + + for _, ifaceSingle := range interfaceList { + joinMulticastGroup(&ifaceSingle) + } + } + + go network.MulticastIPv6Listen() + + return nil +} + +// MulticastIPv6Listen listens for incoming multicast packets +// Fork from network.Listen! Keep any changes synced. +func (network *Network) MulticastIPv6Listen() { + for { + // Buffer: Must be created for each packet as it is passed as pointer. + // If the buffer is too small, ReadFromUDP only reads until its length and returns this error: "wsarecvfrom: A message sent on a datagram socket was larger than the internal message buffer or some other network limit, or the buffer used to receive a datagram into was smaller than the datagram itself." + buffer := make([]byte, maxPacketSize) + length, sender, err := network.multicastSocket.ReadFrom(buffer) + + if err != nil { + log.Printf("Listen Error receiving UDP message: %v\n", err) // Only log for debug purposes. + time.Sleep(time.Millisecond * 50) // In case of endless errors, prevent ddos of CPU. + continue + } + + // skip incoming packets that were looped back + if IsAddressSelf(sender.(*net.UDPAddr)) { + continue + } + + //fmt.Printf("MulticastIPv6Listen from %s at network %s\n", sender.String(), network.address.String()) + + if length < packetLengthMin { + // Discard packets that do not meet the minimum length. + continue + } + + // send the packet to a channel which is processed by multiple workers. + rawPacketsIncoming <- networkWire{network: network, sender: sender.(*net.UDPAddr), raw: buffer[:length], receiverPublicKey: ipv6MulticastPublicKey} + } +} + +// MulticastIPv6Send sends out a single multicast messages to discover peers at the same site +func (network *Network) MulticastIPv6Send() (err error) { + raw, err := packetEncrypt(peerPrivateKey, ipv6MulticastPublicKey, &packetRaw{Protocol: 0, Command: 0}) + if err != nil { + return err + } + + // send out the wire + return network.send(network.multicastIP, ipv6MulticastPort, raw) +} diff --git a/Network Init.go b/Network Init.go new file mode 100644 index 0000000..0904cb5 --- /dev/null +++ b/Network Init.go @@ -0,0 +1,177 @@ +/* +File Name: Network Init.go +Copyright: 2021 Peernet Foundation s.r.o. +Author: Peter Kleissner + +Magic 🪄 to start the network configuration with 0 manual input. Users may specify the list of IPs (and optional ports) to listen; otherwise it listens on all. +IPv6 is always preferred. +*/ + +package core + +import ( + "errors" + "log" + "math/rand" + "net" + "strconv" + "strings" + "time" + + "github.com/btcsuite/btcd/btcec" +) + +// networkWire is an incoming packet +type networkWire struct { + network *Network // network which received the packet + sender *net.UDPAddr // sender of the packet + receiverPublicKey *btcec.PublicKey // public key associated with the receiver + raw []byte // buffer +} + +var ( + rawPacketsIncoming chan networkWire // channel for processing incoming decoded packets by workers + ipsListen map[string]struct{} // list of IPs currently listening on +) + +// initNetwork sets up the network configuration and starts listening. +func initNetwork() { + rawPacketsIncoming = make(chan networkWire, 1000) // buffer up to 1000 UDP packets before they get buffered by the OS network stack and eventually dropped + ipsListen = make(map[string]struct{}) + rand.Seed(time.Now().UnixNano()) // we are not using "crypto/rand" for speed tradeoff + + if config.ListenWorkers == 0 { + config.ListenWorkers = 2 + } + for n := 0; n < config.ListenWorkers; n++ { + go packetWorker(rawPacketsIncoming) + } + + // check if user specified where to listen + if len(config.Listen) > 0 { + for _, listenA := range config.Listen { + host, portA, err := net.SplitHostPort(listenA) + if err != nil && strings.Contains(err.Error(), "missing port in address") { // port is optional + host = listenA + portA = "0" + } else if err != nil { + log.Printf("initNetwork Error invalid input listen address '%s': %s\n", listenA, err.Error()) + continue + } + + portI, _ := strconv.Atoi(portA) + + if _, err := networkPrepareListen(host, portI); err != nil { + log.Printf("initNetwork Error listen on '%s': %s\n", listenA, err.Error()) + continue + } + } + + return + } + + // Listen on all IPv4 and IPv6 addresses + //if _, err := networkPrepareListen("0.0.0.0", 0); err != nil { + // log.Printf("initNetwork Error listen on all IPv4 addresses (0.0.0.0): %s\n", err.Error()) + //} + //if _, err := networkPrepareListen("::", 0); err != nil { + // log.Printf("initNetwork Error listen on all IPv6 addresses (::): %s\n", err.Error()) + //} + + // Listen on each network adapter on each IP. This guarantees the highest deliverability, even though it brings on additional challenges such as: + // * Packet duplicates on IPv6 Multicast (listening on multiple IPs and joining the group on the same adapter) and IPv4 Broadcast (listening on multiple IPs on the same adapter). + // * Local peers are more likely to connect on the same adapter via multiple IPs (i.e. link-local and others, including public IPv6 and temporary public IPv6). + // * Network adapters and IPs might change. Simplest case is if someone changes Wifi network. + interfaceList, err := net.Interfaces() + if err != nil { + log.Printf("initNetwork enumerating network adapters failed: %s\n", err.Error()) + return + } + + for _, iface := range interfaceList { + addresses, err := iface.Addrs() + if err != nil { + log.Printf("initNetwork error enumerating IPs for network adapter '%s': %s\n", iface.Name, err.Error()) + continue + } + + for _, address := range addresses { + net1 := address.(*net.IPNet) + + // Do not listen on lookpback IPs. They are not even needed for discovery of machine-local peers (they will be discovered via regular multicast/broadcast). + if net1.IP.IsLoopback() { + continue + } + + netw, err := networkPrepareListen(net1.IP.String(), 0) + + if err != nil { + // Do not log common errors: + // * "listen udp4 169.254.X.X:X: bind: The requested address is not valid in its context." + // Windows reports link-local addresses for inactive network adapters. + if net1.IP.IsLinkLocalUnicast() { + continue + } + + log.Printf("initNetwork error listening on network adapter '%s' IPv4 '%s': %s\n", iface.Name, net1.IP.String(), err.Error()) + continue + } + + addListenAddress(netw.address) + + log.Printf("Listen on UDP %s\n", netw.address.String()) + } + } +} + +// networkPrepareListen prepares to listen on the given IP address. If port is 0, one is chosen automatically. +func networkPrepareListen(ipA string, port int) (network *Network, err error) { + ip := net.ParseIP(ipA) + if ip == nil { + return nil, errors.New("Invalid input IP") + } + + network = new(Network) + + // get the network interface that belongs to the IP + if ipA != "0.0.0.0" && ipA != "::" { + network.iface, network.ipnet = FindInterfaceByIP(ip) + if network.iface == nil { + return nil, errors.New("Error finding the network interface belonging to IP") + } + } + + // open up the port + if err = network.AutoAssignPort(ip, port); err != nil { + return nil, err + } + + // Success - port is open. Add to the list and start accepting incoming messages. + if IsIPv4(ip) { + networks4 = append(networks4, network) + network.BroadcastIPv4() + } else { + networks6 = append(networks6, network) + network.MulticastIPv6Join() + } + + go network.Listen() + + return network, nil +} + +func addListenAddress(addr *net.UDPAddr) { + ipsListen[net.JoinHostPort(addr.IP.String(), strconv.Itoa(addr.Port))] = struct{}{} +} + +// IsAddressSelf checks if the senders address is actually listening address. This prevents loopback packets from being considered. +// Note: This does not work when listening on 0.0.0.0 or ::1 and binding the sending socket to that. +func IsAddressSelf(addr *net.UDPAddr) bool { + if addr == nil { + return false + } + + // do not use addr.String() since it addds the Zone for IPv6 which may be ambiguous (can be adapter name or address literal). + _, ok := ipsListen[net.JoinHostPort(addr.IP.String(), strconv.Itoa(addr.Port))] + return ok +} diff --git a/Network.go b/Network.go new file mode 100644 index 0000000..532dd65 --- /dev/null +++ b/Network.go @@ -0,0 +1,173 @@ +/* +File Name: Network.go +Copyright: 2021 Peernet Foundation s.r.o. +Author: Peter Kleissner +*/ + +package core + +import ( + "errors" + "log" + "net" + "sync/atomic" + "time" +) + +// Network is a connection adapter through one network interface (adapter). +// Note that for each IP on the same adapter separate network entries are created. +type Network struct { + iface *net.Interface // Network interface belonging to the IP. May not be set. + ipnet *net.IPNet // IP network the listening address belongs to. May not be set. + address *net.UDPAddr // IP:Port where the server listens + socket *net.UDPConn // active socket for send/receive + multicastIP net.IP // Multicast IP, IPv6 only. + multicastSocket net.PacketConn // Multicast socket, IPv6 only. + broadcastSocket net.PacketConn // Broadcast socket, IPv4 only. + broadcastIPv4 []net.IP // Broadcast IPs, IPv4 only. +} + +// networks is a list of all connected networks +var networks4, networks6 []*Network + +// Default ports to use. This may be randomized in the future to prevent fingerprinting (and subsequent blocking) by corporate and ISP firewalls. +const defaultPort = 'p' // 112 + +// AutoAssignPort assigns a port for the given IP. Use port 0 for zero configuration. +func (network *Network) AutoAssignPort(ip net.IP, port int) (err error) { + networkA := "udp6" + if IsIPv4(ip) { + networkA = "udp4" + } + + // A common error return is "bind: The requested address is not valid in its context.". + // This error was observed when the network interface might not be ready after boot but also when listening on a link-local IPv4 (169.254.) for an inactive adapter. + // Previously the algorithm retried up to n times, but this would unnecessarily delay startup in case the IP is actual unlistenable. + connectPortTry := func(port int) (address *net.UDPAddr, socket *net.UDPConn, err error) { + address = &net.UDPAddr{IP: ip, Port: port} + if socket, err = net.ListenUDP(networkA, address); err != nil { + return nil, nil, err + } + + if port == 0 { + localAddr := socket.LocalAddr() + if localAddr == nil { + return nil, nil, errors.New("invalid port assignment") + } + address.Port = localAddr.(*net.UDPAddr).Port + } + + return address, socket, nil + } + + if port != 0 { + network.address, network.socket, err = connectPortTry(port) + return err + } + + // try default main port, then random + if network.address, network.socket, err = connectPortTry(defaultPort); err == nil { + return nil + } + + if network.address, network.socket, err = connectPortTry(0); err == nil { + return nil + } + + return err +} + +// send sends a message +func (network *Network) send(IP net.IP, port int, raw []byte) (err error) { + _, err = network.socket.WriteTo(raw, &net.UDPAddr{IP: IP, Port: port}) + return err +} + +// Currently packets are maxed at 4 KB. This is going to be refined. +const maxPacketSize = 4096 + +// Listen starts listening for incoming packets on the given UDP connection +func (network *Network) Listen() { + for { + // Buffer: Must be created for each packet as it is passed as pointer. + // If the buffer is too small, ReadFromUDP only reads until its length and returns this error: "wsarecvfrom: A message sent on a datagram socket was larger than the internal message buffer or some other network limit, or the buffer used to receive a datagram into was smaller than the datagram itself." + buffer := make([]byte, maxPacketSize) + length, sender, err := network.socket.ReadFromUDP(buffer) + + if err != nil { + log.Printf("Listen Error receiving UDP message: %v\n", err) // Only log for debug purposes. + time.Sleep(time.Millisecond * 50) // In case of endless errors, prevent ddos of CPU. + continue + } + + if length < packetLengthMin { + // Discard packets that do not meet the minimum length. + continue + } + + // send the packet to a channel which is processed by multiple workers. + rawPacketsIncoming <- networkWire{network: network, sender: sender, raw: buffer[:length], receiverPublicKey: peerPublicKey} + } +} + +// packetWorker handles incoming packets. +func packetWorker(packets <-chan networkWire) { + for packet := range packets { + decoded, senderPublicKey, err := packetDecrypt(packet.raw, packet.receiverPublicKey) + if err != nil { + log.Printf("packetWorker Error decrypting packet from '%s': %s\n", packet.sender.String(), err.Error()) + continue + } + + // immediately discard message if sender = self + if senderPublicKey.IsEqual(peerPublicKey) { + continue + } + + // supported protocol version + if decoded.Protocol != 0 { + continue + } + + peer := PeerlistLookup(senderPublicKey) + if peer != nil { + // Existing peers: Update statistics and network address if new + atomic.AddUint64(&peer.StatsPacketReceived, 1) + peer.registerConnection(packet.sender, packet.network) + } + + // process the packet + message := &packet2{SenderPublicKey: senderPublicKey, packetRaw: *decoded, network: packet.network, address: packet.sender} + + switch decoded.Command { + case 0: // Announce + peer.announcement(message) + + case 1: // Response + peer.response(message) + + case 10: // Chat [debug] + peer.chat(message) + + default: // Unknown command + + } + + } +} + +// GetNetworks returns the list of connected networks +func GetNetworks(networkType int) (networks []*Network) { + switch networkType { + case 4: + return networks4 + case 6: + return networks6 + } + return nil +} + +// GetListen returns connectivity information +func (network *Network) GetListen() (listen *net.UDPAddr, multicastIPv6 net.IP, broadcastIPv4 []net.IP) { + return network.address, network.multicastIP, network.broadcastIPv4 +} diff --git a/Packet Encoding.go b/Packet Encoding.go new file mode 100644 index 0000000..59a0612 --- /dev/null +++ b/Packet Encoding.go @@ -0,0 +1,149 @@ +/* +File Name: Packet Encoding.go +Copyright: 2021 Peernet Foundation s.r.o. +Author: Peter Kleissner + +Basic packet structure of ALL packets: +Offset Size Info +0 4 Nonce +4 1 Protocol version = 0 +5 1 Command +6 2 Size of payload data +8 ? Payload + ? Randomized garbage +? 65 Signature, ECDSA secp256k1 512-bit + 1 header byte + +The peer ID of the sender, which is a ECDSA (secp256k1) 257-bit public key, can be extracted from the ECDSA signature. +The signature is applied on the entire packet, which guarantees that the signature becomes invalid should someone try to forge the receiver (i.e. forward the packet). +Because the signature could be a possible fingerpint, it is encrypted itself. +*/ + +package core + +import ( + "encoding/binary" + "errors" + "math/rand" + + "github.com/btcsuite/btcd/btcec" + "golang.org/x/crypto/salsa20" + "lukechampine.com/blake3" +) + +// packetRaw is a decrypted P2P message +type packetRaw struct { + Protocol uint8 // Protocol version = 0 + Command uint8 // 0 = Announcement + Payload []byte // Payload +} + +// The minimum packet size is 8 bytes (minimum header size) + 65 bytes (signature) +const packetLengthMin = 8 + signatureSize +const signatureSize = 65 +const maxRandomGarbage = 20 + +// packetDecrypt decrypts the packet, verifies its signature and returns a high-level version of the packet. +func packetDecrypt(raw []byte, receiverPublicKey *btcec.PublicKey) (packet *packetRaw, senderPublicKey *btcec.PublicKey, err error) { + // Packet is assumed to be already checked for minimum length. + + // Prepare Salsa20 nonce and key. Nonce = 2x first 4 bytes. For size reasons, only 4 bytes (instead of 8 bytes) is supplied in the packet. + // This could be a risk, but considering we only use the PUBLIC key as decryption key, it is negligible. + nonce := make([]byte, 8) + copy(nonce[0:4], raw[0:4]) + copy(nonce[4:8], raw[0:4]) + + // Verify the signature and extract the public key from it. + var signature [signatureSize]byte + copy(signature[:], raw[len(raw)-signatureSize:]) + keySalsa := publicKeyToSalsa20Key(receiverPublicKey) + salsa20.XORKeyStream(signature[:], signature[:], nonce, keySalsa) + + senderPublicKey, _, err = btcec.RecoverCompact(btcec.S256(), signature[:], hashData(raw[:len(raw)-signatureSize])) + if err != nil { + return nil, nil, err + } + + // Decrypt the packet using Salsa20. + bufferDecrypted := make([]byte, len(raw)-signatureSize-4) // full length -signature -nonce + salsa20.XORKeyStream(bufferDecrypted[:], raw[4:len(raw)-signatureSize], nonce, keySalsa) + + // copy all fields + packet = &packetRaw{Protocol: bufferDecrypted[0], Command: bufferDecrypted[1]} + + sizePayload := binary.LittleEndian.Uint16(bufferDecrypted[2:4]) + if int(sizePayload) > len(bufferDecrypted)-4 { // invalid length? + return nil, nil, errors.New("invalid length field") + } + if sizePayload > 0 { + packet.Payload = make([]byte, int(sizePayload)) + copy(packet.Payload, bufferDecrypted[4:4+int(sizePayload)]) + } + + return packet, senderPublicKey, nil +} + +// packetEncrypt encrypts a packet using the provided senders private key and receivers compressed public key. +func packetEncrypt(senderPrivateKey *btcec.PrivateKey, receiverPublicKey *btcec.PublicKey, packet *packetRaw) (raw []byte, err error) { + garbage := packetGarbage(packetLengthMin + len(packet.Payload)) + raw = make([]byte, packetLengthMin+len(packet.Payload)+len(garbage)) + + nonceC := rand.Uint32() + nonce := make([]byte, 8) + binary.LittleEndian.PutUint32(nonce[0:4], nonceC) + binary.LittleEndian.PutUint32(nonce[4:8], nonceC) + copy(raw[0:4], nonce[0:4]) + + raw[4] = packet.Protocol + raw[5] = packet.Command + + binary.LittleEndian.PutUint16(raw[6:8], uint16(len(packet.Payload))) + copy(raw[8:], packet.Payload) + copy(raw[8+len(packet.Payload):8+len(packet.Payload)+len(garbage)], garbage) + + // encrypt it using Salsa20 + keySalsa := publicKeyToSalsa20Key(receiverPublicKey) + salsa20.XORKeyStream(raw[4:8+len(packet.Payload)+len(garbage)], raw[4:8+len(packet.Payload)+len(garbage)], nonce, keySalsa) + + // add signature + signature, err := btcec.SignCompact(btcec.S256(), senderPrivateKey, hashData(raw[:len(raw)-signatureSize]), true) + if err != nil { + return nil, err + } + + salsa20.XORKeyStream(signature[:], signature[:], nonce, keySalsa) + copy(raw[len(raw)-signatureSize:], signature) + + return raw, nil +} + +func packetGarbage(packetLength int) (random []byte) { + // Align maximum length at 508 bytes (UDP minimum no fragmentation) and 1472 bytes (MTU). + maxLength := maxRandomGarbage + switch { + case packetLength == 508, packetLength == 1472: + return nil + case packetLength < 508 && (508-packetLength) < maxRandomGarbage: + maxLength = packetLength - 508 + case packetLength < 1472 && (1472-packetLength) < maxRandomGarbage: + maxLength = packetLength - 1472 + } + + b := make([]byte, rand.Intn(maxLength)) + if _, err := rand.Read(b); err != nil { + return nil + } + return b +} + +func publicKeyToSalsa20Key(publicKey *btcec.PublicKey) (key *[32]byte) { + // bit 0 from PublicKey.Y is ignored here, but is negligible for this purpose + key = new([32]byte) + copy(key[:], publicKey.SerializeCompressed()[1:]) + return key +} + +// hashData abstracts the hash function. +func hashData(data []byte) (hash []byte) { + hash32 := blake3.Sum256(data) + return hash32[:] +} diff --git a/Peer ID.go b/Peer ID.go new file mode 100644 index 0000000..538a3f9 --- /dev/null +++ b/Peer ID.go @@ -0,0 +1,263 @@ +/* +File Name: Peer ID.go +Copyright: 2021 Peernet Foundation s.r.o. +Author: Peter Kleissner +*/ + +package core + +import ( + "encoding/hex" + "errors" + "log" + "net" + "os" + "sync" + "sync/atomic" + + "github.com/btcsuite/btcd/btcec" +) + +// peerID is the current peers ID. It is a ECDSA (secp256k1) 257-bit public key. +var peerPrivateKey *btcec.PrivateKey +var peerPublicKey *btcec.PublicKey + +func initPeerID() { + peerList = make(map[[btcec.PubKeyBytesLenCompressed]byte]*PeerInfo) + + // load existing key from config, if available + if len(config.PrivateKey) > 0 { + configPK, err := hex.DecodeString(config.PrivateKey) + if err == nil { + peerPrivateKey, peerPublicKey = btcec.PrivKeyFromBytes(btcec.S256(), configPK) + return + } + + log.Printf("Private key in config is corrupted! Error: %s\n", err.Error()) + os.Exit(1) + } + + // if the peer ID is empty, create a new user public-private key pair + var err error + peerPrivateKey, peerPublicKey, err = Secp256k1NewPrivateKey() + if err != nil { + log.Printf("Error generating public-private key pairs: %s\n", err.Error()) + os.Exit(1) + } + + // save the newly generated private key into the config + config.PrivateKey = hex.EncodeToString(peerPublicKey.SerializeCompressed()) + + saveConfig() +} + +// Secp256k1NewPrivateKey creates a new public-private key pair +func Secp256k1NewPrivateKey() (privateKey *btcec.PrivateKey, publicKey *btcec.PublicKey, err error) { + key, err := btcec.NewPrivateKey(btcec.S256()) + if err != nil { + return nil, nil, err + } + + return key, (*btcec.PublicKey)(&key.PublicKey), nil +} + +// ExportPrivateKey returns the peers public and private key +func ExportPrivateKey() (privateKey *btcec.PrivateKey, publicKey *btcec.PublicKey) { + return peerPrivateKey, peerPublicKey +} + +// Connection is an established connection between a remote IP address and a local network adapter. +// New connections may only be created in case of successful INCOMING packets. +type Connection struct { + Network *Network // network which received the packet + Address *net.UDPAddr // address of the sender or receiver +} + +// PeerInfo stores information about a single remote peer +type PeerInfo struct { + PublicKey *btcec.PublicKey // Public key + Connections []*Connection // list of established connections to the peer + connectionLatest *Connection // Latest valid connection. Allows to switch on the fly between connections; if one becomes invalid, try others. + + // statistics + StatsPacketSent uint64 // Count of packets sent + StatsPacketReceived uint64 // Count of packets received +} + +var peerList map[[btcec.PubKeyBytesLenCompressed]byte]*PeerInfo +var peerlistMutex sync.RWMutex + +// PeerlistAdd adds a new peer to the peer list. It does not validate the peer info. If the peer is already added, it does nothing. +func PeerlistAdd(PublicKey *btcec.PublicKey, connections ...*Connection) (peer *PeerInfo, added bool) { + if len(connections) == 0 { + return nil, false + } + + peerlistMutex.Lock() + defer peerlistMutex.Unlock() + + peer, ok := peerList[publicKey2Compressed(PublicKey)] + if ok { + return peer, false + } + + peer = &PeerInfo{PublicKey: PublicKey, Connections: connections, connectionLatest: connections[0]} + peerList[publicKey2Compressed(peer.PublicKey)] = peer + + return peer, true +} + +// PeerlistRemove removes a peer from the peer list. +func PeerlistRemove(peer *PeerInfo) { + peerlistMutex.Lock() + defer peerlistMutex.Unlock() + + delete(peerList, publicKey2Compressed(peer.PublicKey)) +} + +// PeerlistGet returns the full peer list +func PeerlistGet() (peers []*PeerInfo) { + peerlistMutex.RLock() + defer peerlistMutex.RUnlock() + + for _, peer := range peerList { + peers = append(peers, peer) + } + + return peers +} + +// PeerlistLookup returns the peer from the list with the public key +func PeerlistLookup(publicKey *btcec.PublicKey) (peer *PeerInfo) { + peerlistMutex.RLock() + defer peerlistMutex.RUnlock() + + peer, _ = peerList[publicKey2Compressed(publicKey)] + return peer +} + +// PeerlistCount returns the current count of peers in the peer list +func PeerlistCount() (count int) { + peerlistMutex.RLock() + defer peerlistMutex.RUnlock() + + return len(peerList) +} + +func publicKey2Compressed(publicKey *btcec.PublicKey) [btcec.PubKeyBytesLenCompressed]byte { + var key [btcec.PubKeyBytesLenCompressed]byte + copy(key[:], publicKey.SerializeCompressed()) + return key +} + +// send sends a raw packet to the peer +func (peer *PeerInfo) send(packet *packetRaw) (err error) { + if len(peer.Connections) == 0 { + return errors.New("no valid connection to peer") + } + + packet.Protocol = 0 + + raw, err := packetEncrypt(peerPrivateKey, peer.PublicKey, packet) + if err != nil { + return err + } + + atomic.AddUint64(&peer.StatsPacketSent, 1) + + // Send out the wire. Use connectionLatest if available. + // Failover: If sending fails and there are other connections available, try those. Automatically update connectionLatest if one is successful. + // Windows: This works great on if the adapter is disabled, however, does not detect if the network cable is unplugged. + if peer.connectionLatest != nil { + useConnection := peer.connectionLatest + if err = useConnection.Network.send(useConnection.Address.IP, useConnection.Address.Port, raw); err != nil && len(peer.Connections) > 1 { + err = peer.sendFailover(useConnection, raw) + } + return err + } + + if err = peer.Connections[0].Network.send(peer.Connections[0].Address.IP, peer.Connections[0].Address.Port, raw); err != nil && len(peer.Connections) > 1 { + err = peer.sendFailover(peer.Connections[0], raw) + } + return err +} + +// sendFailover tries to send the packet over any other different (than the failed) connection. +// If send works, it will switch over connectionLatest. +// Windows: A common error when the network adapter is disabled is "wsasendto: The requested address is not valid in its context". +func (peer *PeerInfo) sendFailover(failed *Connection, raw []byte) (err error) { + for _, connection := range peer.Connections { + if connection != failed { + err = connection.Network.send(connection.Address.IP, connection.Address.Port, raw) + if err == nil { + peer.connectionLatest = connection + return nil + } + } + } + return err +} + +// sendAllNetworks sends a raw packet via all networks +func sendAllNetworks(receiverPublicKey *btcec.PublicKey, packet *packetRaw, remote *net.UDPAddr) (err error) { + packet.Protocol = 0 + raw, err := packetEncrypt(peerPrivateKey, receiverPublicKey, packet) + if err != nil { + return err + } + + successCount := 0 + + if IsIPv6(remote.IP.To16()) { + for _, network := range networks6 { + // Do not mix link-local unicast targets with non link-local networks (only when iface is known, i.e. not catch all local) + if network.iface != nil && remote.IP.IsLinkLocalUnicast() != network.address.IP.IsLinkLocalUnicast() { + continue + } + + err = network.send(remote.IP, remote.Port, raw) + if err == nil { + successCount++ + } + } + } else { + for _, network := range networks4 { + // Do not mix link-local unicast targets with non link-local networks (only when iface is known, i.e. not catch all local) + if network.iface != nil && remote.IP.IsLinkLocalUnicast() != network.address.IP.IsLinkLocalUnicast() { + continue + } + + err = network.send(remote.IP, remote.Port, raw) + if err == nil { + successCount++ + } + } + } + + if successCount == 0 { + return errors.New("no successful send") + } + + return nil +} + +// registerConnection registers an incoming connection for an existing peer. If new, it will add to the list. +// Currently there is only a connectionLatest indicating the latest valid connection. In the future other priority such as last packet time, speed, local vs remote IP might be used. +func (peer *PeerInfo) registerConnection(remote *net.UDPAddr, network *Network) { + for n := range peer.Connections { + if peer.Connections[n].Address.IP.Equal(remote.IP) && peer.Connections[n].Network.address.IP.Equal(network.address.IP) { + // Connection already established. Verify port and update if necessary. + // Some NATs may rotate ports. Some mobile phone providers even rotate IPs which is not detected here. + if peer.Connections[n].Address.Port != remote.Port { + peer.Connections[n].Address.Port = remote.Port + } + + peer.connectionLatest = peer.Connections[n] + return + } + } + + peer.Connections = append(peer.Connections, &Connection{Address: remote, Network: network}) + + peer.connectionLatest = peer.Connections[len(peer.Connections)-1] +} diff --git a/Peernet.go b/Peernet.go new file mode 100644 index 0000000..593f550 --- /dev/null +++ b/Peernet.go @@ -0,0 +1,22 @@ +/* +File Name: Peernet.go +Copyright: 2021 Peernet Foundation s.r.o. +Author: Peter Kleissner +*/ + +package core + +func init() { + loadConfig() + initPeerID() + initMulticastIPv6() + initBroadcastIPv4() + initNetwork() + initSeedList() +} + +// Connect starts bootstrapping and local peer discovery. +func Connect() { + go bootstrap() + go autoMulticastBroadcast() +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff24841 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Peernet Core + +The core library which is needed for any Peernet application. It provides connectivity to the network and all basic functions. + +Current version: 0.1 (pre-alpha) + +## Encryption and Hashing functions + +* Salsa20 is used for encrypting the packets. +* secp256k1 is used to generate the peer IDs (public keys). +* blake3 is used for hashing the packets when signing. + +## Dependencies + +Before compiling, make sure to download and update all 3rd party packages: + +``` +go get -u github.com/gofrs/uuid +go get -u github.com/btcsuite/btcd/btcec +go get -u github.com/libp2p/go-reuseport +``` + +## Configuration + +Peernet follows a "zeroconf" approach, meaning there is no manual configuration required. However, in certain cases such as providing root peers [1] that shall listen on a fixed IP and port, it is desirable to create a config file. + +The name of the config file is hard-coded to `Settings.yaml`. If it does not exist, it will be created with default values. It uses the YAML format. Any public/private keys in the config are hex encoded. Here are some notable settings: + +* `ListenWorkers` defines the count of concurrent workers processing packets (decrypting them and then taking action). Default 2. +* `Listen` defines IP:Port combinations to listen on. If not specified, it will listen on all IPs. You can specify an IP but port 0 for auto port selection. IPv6 addresses must be in the format "[IPv6]:Port". + +[1] Root peer = A peer operated by a known trusted entity. They allow to speed up the network including discovery of peers and data. + +## Contributing + +Please note that by contributing code, documentation, ideas, snippets, or any other intellectual property you agree that you have all the necessary rights and you agree that we, the Peernet Foundation, may use it for any purpose. + +© 2021 Peernet Foundation diff --git a/Settings.go b/Settings.go new file mode 100644 index 0000000..8ceed7b --- /dev/null +++ b/Settings.go @@ -0,0 +1,83 @@ +/* +File Name: Settings.go +Copyright: 2021 Peernet Foundation s.r.o. +Author: Peter Kleissner +*/ + +package core + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "strings" + + "gopkg.in/yaml.v3" +) + +const configFile = "Settings.yaml" +const Version = "0.1" + +var config struct { + LogFile string `yaml:"LogFile"` // Log file + + Listen []string `yaml:"Listen"` // IP:Port combinations + ListenWorkers int `yaml:"ListenWorkers"` // Count of workers to process incoming raw packets. Default 2. + + // User specific settings + PrivateKey string `yaml:"PrivateKey"` // The Private Key, hex encoded so it can be copied manually + + // Initial peer seed list + SeedList []peerSeed `yaml:"SeedList"` +} + +// peerSeed is a singl peer entry from the config's seed list +type peerSeed struct { + PublicKey string `yaml:"PublicKey"` // Public key = peer ID. Hex encoded. + Address []string `yaml:"Address"` // IP:Port +} + +// loadConfig reads the YAML configuration file +func loadConfig() { + cfg, err := ioutil.ReadFile(configFile) + if err != nil && strings.Contains(err.Error(), "system cannot find the file specified") { + // Does not exist. Use default values. + config.LogFile = "Log.txt" + + } else if err != nil { + fmt.Printf("Configuration file '%s' could not be read. Error: %s\n", configFile, err.Error()) + os.Exit(1) + } else { + // read existing config + err = yaml.Unmarshal(cfg, &config) + if err != nil { + fmt.Printf("Configuration file '%s' could not be read. Please make sure it contains valid YAML data. Error: %v\n", configFile, err.Error()) + os.Exit(1) + } + } + + // redirect all output to the log file + logFile, err := os.OpenFile(config.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + fmt.Printf("Error creating log file '%s': %v\n", config.LogFile, err) + } + // defer logFile.Close() // has to remain open until program closes + + log.SetOutput(logFile) + log.Printf("---- Peernet Command-Line Client " + Version + " ----\n") +} + +func saveConfig() { + data, err := yaml.Marshal(config) + if err != nil { + log.Printf("saveConfig Error marshalling config: %v\n", err.Error()) + return + } + + err = ioutil.WriteFile(configFile, data, 0644) + if err != nil { + log.Printf("saveConfig Error writing config '%s': %v\n", configFile, err.Error()) + return + } +}