mirror of
https://github.com/hyprspace/hyprspace.git
synced 2026-09-14 02:56:21 +05:00
Hyprspace previously relied solely on Kademlia DHT with public bootstrap nodes for peer discovery, requiring internet access for any peers to connect. This adds libp2p mDNS discovery so that peers on the same LAN can find each other without external infrastructure. When a peer is discovered via mDNS and matches a configured VPN peer, its addresses are added to the peerstore and the connection loop is triggered immediately.
526 lines
13 KiB
Go
526 lines
13 KiB
Go
package node
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/hyprspace/hyprspace/config"
|
|
hsdns "github.com/hyprspace/hyprspace/dns"
|
|
"github.com/hyprspace/hyprspace/p2p"
|
|
hsrpc "github.com/hyprspace/hyprspace/rpc"
|
|
"github.com/hyprspace/hyprspace/svc"
|
|
"github.com/hyprspace/hyprspace/tun"
|
|
"github.com/ipfs/go-log/v2"
|
|
"github.com/libp2p/go-cidranger"
|
|
dht "github.com/libp2p/go-libp2p-kad-dht"
|
|
"github.com/libp2p/go-libp2p/core/connmgr"
|
|
"github.com/libp2p/go-libp2p/core/event"
|
|
"github.com/libp2p/go-libp2p/core/host"
|
|
"github.com/libp2p/go-libp2p/core/network"
|
|
"github.com/libp2p/go-libp2p/core/peer"
|
|
"github.com/multiformats/go-multiaddr"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
var logger = log.Logger("hyprspace/node")
|
|
|
|
type SharedStream struct {
|
|
Stream *network.Stream
|
|
Lock *sync.Mutex
|
|
}
|
|
|
|
type Node struct {
|
|
cfg *config.Config
|
|
p2p host.Host
|
|
dht *dht.IpfsDHT
|
|
tunDev *tun.TUN
|
|
activeStreams map[peer.ID]SharedStream
|
|
activeStreamsLock sync.RWMutex
|
|
ctx context.Context
|
|
cancel func()
|
|
lockPath string
|
|
configPath string
|
|
interfaceName string
|
|
wg *sync.WaitGroup
|
|
}
|
|
|
|
func New(ctx context.Context, configPath string, ifName string) Node {
|
|
innerCtx, ctxCancel := context.WithCancel(ctx)
|
|
|
|
return Node{
|
|
cfg: &config.Config{},
|
|
p2p: nil,
|
|
tunDev: &tun.TUN{},
|
|
ctx: innerCtx,
|
|
cancel: ctxCancel,
|
|
configPath: configPath,
|
|
interfaceName: ifName,
|
|
}
|
|
}
|
|
|
|
func (node *Node) Run() error {
|
|
// Read in configuration from file.
|
|
cfg2, err := config.Read(node.configPath)
|
|
if err != nil {
|
|
logger.With(err).Error("Failed to read config")
|
|
return err
|
|
}
|
|
|
|
cfg2.Interface = node.interfaceName
|
|
node.cfg = cfg2
|
|
|
|
logger.Info("Creating TUN Device")
|
|
|
|
// Create new TUN device
|
|
node.tunDev, err = tun.New(
|
|
node.cfg.Interface,
|
|
tun.Address(node.cfg.BuiltinAddr4.String()+"/32"),
|
|
tun.Address(node.cfg.BuiltinAddr6.String()+"/128"),
|
|
tun.MTU(1420),
|
|
)
|
|
if err != nil {
|
|
logger.With(err).Error("Failed to create TUN Device")
|
|
return err
|
|
}
|
|
allRoutes4, err := node.cfg.PeerLookup.ByRoute.CoveredNetworks(*cidranger.AllIPv4)
|
|
if err != nil {
|
|
logger.With(err).Error("Failed to lookup IPv4 peer-routes")
|
|
return err
|
|
}
|
|
allRoutes6, err := node.cfg.PeerLookup.ByRoute.CoveredNetworks(*cidranger.AllIPv6)
|
|
if err != nil {
|
|
logger.With(err).Error("Failed to lookup IPv6 peer-routes")
|
|
return err
|
|
}
|
|
var routeOpts []tun.Option
|
|
|
|
for _, r := range allRoutes4 {
|
|
routeOpts = append(routeOpts, tun.Route(r.Network()))
|
|
}
|
|
for _, r := range allRoutes6 {
|
|
routeOpts = append(routeOpts, tun.Route(r.Network()))
|
|
}
|
|
|
|
recursionGater := p2p.NewRecursionGater(node.cfg)
|
|
var gater connmgr.ConnectionGater
|
|
if node.cfg.FilterPrivateAddresses {
|
|
gater = p2p.NewMultiGater(
|
|
recursionGater,
|
|
p2p.NewFilterGater(
|
|
// IPv4 local
|
|
parseCIDR("10.0.0.0/8"),
|
|
parseCIDR("172.16.0.0/12"),
|
|
parseCIDR("192.168.0.0/16"),
|
|
// IPv4 link-local
|
|
parseCIDR("169.254.0.0/16"),
|
|
// IPv4 loopback
|
|
parseCIDR("127.0.0.0/8"),
|
|
// IPv6 link-local
|
|
parseCIDR("fe80::/10"),
|
|
// IPv6 loopback
|
|
parseCIDR("::1/128"),
|
|
),
|
|
)
|
|
} else {
|
|
gater = recursionGater
|
|
}
|
|
|
|
logger.Info("Creating LibP2P node")
|
|
|
|
// Create P2P Node
|
|
node.p2p, node.dht, err = p2p.CreateNode(
|
|
node.ctx,
|
|
node.cfg.PrivateKey,
|
|
node.cfg.ListenAddresses,
|
|
node.cfg.BootstrapPeers,
|
|
node.streamHandler,
|
|
p2p.NewClosedCircuitRelayFilter(node.cfg.Peers),
|
|
gater,
|
|
node.cfg.Peers,
|
|
)
|
|
if err != nil {
|
|
logger.With(err).Error("Failed to create Libp2p node")
|
|
return err
|
|
}
|
|
|
|
node.p2p.SetStreamHandler(p2p.PeXProtocol, p2p.NewPeXStreamHandler(node.p2p, node.cfg))
|
|
|
|
for _, p := range node.cfg.Peers {
|
|
node.p2p.ConnManager().Protect(p.ID, "/hyprspace/peer")
|
|
}
|
|
|
|
node.wg = &sync.WaitGroup{}
|
|
|
|
logger.Debug("Setting up Node discovery via DHT")
|
|
|
|
// Setup DHT Discovery
|
|
go p2p.Discover(node.ctx, node.wg, node.p2p, node.dht, node.cfg.Peers)
|
|
|
|
// Setup mDNS Discovery for LAN peers
|
|
err = p2p.SetupMDNS(node.p2p, node.cfg.Peers)
|
|
if err != nil {
|
|
logger.With(err).Warn("Failed to start mDNS discovery")
|
|
}
|
|
|
|
// Configure path for lock
|
|
node.lockPath = filepath.Join(filepath.Dir(node.cfg.Path), node.cfg.Interface+".lock")
|
|
|
|
logger.Debug("Starting Peer-Exchange service")
|
|
// PeX
|
|
go p2p.PeXService(node.ctx, node.wg, node.p2p, node.cfg)
|
|
|
|
logger.Debug("Starting Route Metrics service")
|
|
// Route metrics and latency
|
|
go p2p.RouteMetricsService(node.ctx, node.wg, node.p2p, node.cfg)
|
|
|
|
// Log about various events
|
|
err = node.eventLogger(node.ctx, node.p2p)
|
|
if err != nil {
|
|
logger.With(err).Error("Failed to subscribe to EventBus")
|
|
return err
|
|
}
|
|
|
|
logger.Debug("Starting RPC server")
|
|
// RPC server
|
|
go hsrpc.RpcServer(node.ctx, node.wg, multiaddr.StringCast(fmt.Sprintf("/unix/run/hyprspace-rpc.%s.sock", node.cfg.Interface)), node.p2p, *node.cfg, *node.tunDev)
|
|
|
|
logger.Debug("Starting DNS server")
|
|
// Magic DNS server
|
|
go hsdns.MagicDnsServer(node.ctx, node.wg, *node.cfg, node.p2p)
|
|
|
|
// metrics endpoint
|
|
metricsPort, ok := os.LookupEnv("HYPRSPACE_METRICS_PORT")
|
|
if ok {
|
|
metricsTuple := fmt.Sprintf("127.0.0.1:%s", metricsPort)
|
|
http.Handle("/metrics", promhttp.Handler())
|
|
go func() {
|
|
logger.Debug("Starting metrics API server")
|
|
http.ListenAndServe(metricsTuple, nil)
|
|
}()
|
|
logger.Info(fmt.Sprintf("Listening for metrics scrape requests on http://%s/metrics", metricsTuple))
|
|
}
|
|
|
|
serviceNet := svc.NewServiceNetwork(node.p2p, node.cfg, node.tunDev)
|
|
|
|
for name, service := range node.cfg.Services {
|
|
proxy, err := svc.ProxyTo(service.Target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
serviceNet.Register(
|
|
name,
|
|
proxy,
|
|
)
|
|
}
|
|
|
|
var svcNetIds [][4]byte
|
|
for _, p := range node.cfg.Peers {
|
|
svcNetIds = append(svcNetIds, config.MkNetID(p.ID))
|
|
}
|
|
svcNetIds = append(svcNetIds, config.MkNetID(node.p2p.ID()))
|
|
for _, netId := range svcNetIds {
|
|
addr := make([]byte, 16)
|
|
copy(addr, serviceNet.NetworkRange.IP)
|
|
copy(addr[10:], netId[:])
|
|
mask1, mask0 := serviceNet.NetworkRange.Mask.Size()
|
|
routeOpts = append(routeOpts, tun.Route(net.IPNet{
|
|
IP: addr,
|
|
Mask: net.CIDRMask(mask1+32, mask0),
|
|
}))
|
|
}
|
|
|
|
// Write lock to filesystem to indicate an existing running daemon.
|
|
err = os.WriteFile(node.lockPath, fmt.Append(nil, os.Getpid()), os.ModePerm)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
logger.Debug("Bringing up TUN device")
|
|
// Bring Up TUN Device
|
|
err = node.tunDev.Up()
|
|
if err != nil {
|
|
logger.With(err).Error("Failed to bring TUN device up")
|
|
return errors.New("unable to bring up tun device: " + err.Error())
|
|
}
|
|
err = node.tunDev.Apply(routeOpts...)
|
|
if err != nil {
|
|
return errors.New("unable to apply routing options: " + err.Error())
|
|
}
|
|
|
|
logger.Info("Network setup complete")
|
|
|
|
// Initialize active streams map and packet byte array.
|
|
node.activeStreams = make(map[peer.ID]SharedStream)
|
|
go func() {
|
|
for {
|
|
var packet = make([]byte, 1420)
|
|
// Read in a packet from the tun device.
|
|
plen, err := node.tunDev.Iface.Read(packet)
|
|
if errors.Is(err, fs.ErrClosed) {
|
|
logger.Warn("Interface closed")
|
|
<-node.ctx.Done()
|
|
time.Sleep(1 * time.Second)
|
|
return
|
|
} else if err != nil {
|
|
logger.With(err).Error("Failed to read from interface")
|
|
continue
|
|
}
|
|
|
|
var dstIP net.IP
|
|
proto := packet[0] & 0xf0
|
|
|
|
if proto == 0x40 {
|
|
dstIP = net.IP(packet[16:20])
|
|
if node.cfg.BuiltinAddr4.Equal(dstIP) {
|
|
continue
|
|
}
|
|
} else if proto == 0x60 {
|
|
dstIP = net.IP(packet[24:40])
|
|
if node.cfg.BuiltinAddr6.Equal(dstIP) {
|
|
continue
|
|
} else if serviceNet.NetworkRange.Contains(dstIP) {
|
|
// Are you TCP because your protocol is 6, or is your protocol 6 because you are TCP?
|
|
if packet[6] == 0x06 {
|
|
port := uint16(packet[42])*256 + uint16(packet[43])
|
|
if serviceNet.EnsureListener([16]byte(packet[24:40]), port) {
|
|
count, err := (*serviceNet.Tun).Write([][]byte{packet}, 0)
|
|
if count == 0 || err != nil {
|
|
logger.With(err).Error("Error writing to service-network tunnel")
|
|
}
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
} else {
|
|
continue
|
|
}
|
|
var dst peer.ID
|
|
|
|
// Check route table for destination address.
|
|
route, found := node.cfg.FindRouteForIP(dstIP)
|
|
|
|
if found {
|
|
dst = route.Target.ID
|
|
go node.sendPacket(dst, packet, plen)
|
|
}
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func (node *Node) getActiveStream(pid peer.ID) (SharedStream, bool) {
|
|
node.activeStreamsLock.RLock()
|
|
defer node.activeStreamsLock.RUnlock()
|
|
s, ok := node.activeStreams[pid]
|
|
return s, ok
|
|
}
|
|
|
|
func (node *Node) insertActiveStream(pid peer.ID, ss SharedStream) bool {
|
|
node.activeStreamsLock.Lock()
|
|
defer node.activeStreamsLock.Unlock()
|
|
if _, exists := node.activeStreams[pid]; exists {
|
|
return false
|
|
}
|
|
node.activeStreams[pid] = ss
|
|
return true
|
|
}
|
|
|
|
func (node *Node) expireActiveStream(pid peer.ID) {
|
|
node.activeStreamsLock.Lock()
|
|
defer node.activeStreamsLock.Unlock()
|
|
delete(node.activeStreams, pid)
|
|
}
|
|
|
|
func (node *Node) streamHandler(stream network.Stream) {
|
|
remotePeerID := stream.Conn().RemotePeer()
|
|
|
|
// If the remote node ID isn't in the list of known nodes don't respond.
|
|
if _, ok := config.FindPeer(node.cfg.Peers, remotePeerID); !ok {
|
|
stream.Reset()
|
|
return
|
|
}
|
|
|
|
var streamLock = new(sync.Mutex)
|
|
|
|
// Version 0 nodes don't read from this stream, so we can't reuse it.
|
|
if stream.Protocol() != p2p.ProtocolV0 {
|
|
inserted := node.insertActiveStream(remotePeerID, SharedStream{
|
|
Stream: &stream,
|
|
Lock: streamLock,
|
|
})
|
|
if inserted {
|
|
defer node.expireActiveStream(remotePeerID)
|
|
}
|
|
}
|
|
|
|
var packet = make([]byte, 1420)
|
|
var packetSize = make([]byte, 2)
|
|
for {
|
|
// Read the incoming packet's size as a binary value.
|
|
_, err := stream.Read(packetSize)
|
|
if err != nil {
|
|
stream.Close()
|
|
return
|
|
}
|
|
|
|
// Decode the incoming packet's size from binary.
|
|
size := binary.LittleEndian.Uint16(packetSize)
|
|
|
|
// Read in the packet until completion.
|
|
var plen uint16 = 0
|
|
for plen < size {
|
|
tmp, err := stream.Read(packet[plen:size])
|
|
plen += uint16(tmp)
|
|
if err != nil {
|
|
stream.Close()
|
|
return
|
|
}
|
|
}
|
|
err = stream.SetWriteDeadline(time.Now().Add(25 * time.Second))
|
|
if err != nil {
|
|
logger.With(err).Error("Failed to set write deadline")
|
|
stream.Close()
|
|
return
|
|
}
|
|
_, _ = node.tunDev.Iface.Write(packet[:size])
|
|
}
|
|
}
|
|
|
|
func (node *Node) sendPacket(dst peer.ID, packet []byte, plen int) {
|
|
// Check if we already have an open connection to the destination peer.
|
|
ms, ok := node.getActiveStream(dst)
|
|
if ok {
|
|
if func() bool {
|
|
ms.Lock.Lock()
|
|
defer ms.Lock.Unlock()
|
|
// Write out the packet's length to the libp2p stream to ensure
|
|
// we know the full size of the packet at the other end.
|
|
err := binary.Write(*ms.Stream, binary.LittleEndian, uint16(plen))
|
|
if err == nil {
|
|
// Write the packet out to the libp2p stream.
|
|
// If everyting succeeds continue on to the next packet.
|
|
_, err = (*ms.Stream).Write(packet[:plen])
|
|
if err == nil {
|
|
err := (*ms.Stream).SetWriteDeadline(time.Now().Add(25 * time.Second))
|
|
if err == nil {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
// If we encounter an error when writing to a stream we should
|
|
// close that stream and delete it from the active stream map.
|
|
(*ms.Stream).Close()
|
|
node.expireActiveStream(dst)
|
|
return false
|
|
}() {
|
|
return
|
|
}
|
|
}
|
|
|
|
stream, err := node.p2p.NewStream(node.ctx, dst, p2p.Protocols...)
|
|
if err != nil {
|
|
logger.With(zap.String("destination", dst.String()), zap.Error(err)).Error("Failed to open stream")
|
|
go p2p.Rediscover()
|
|
return
|
|
}
|
|
err = stream.SetWriteDeadline(time.Now().Add(25 * time.Second))
|
|
if err != nil {
|
|
logger.With(err).Error("Failed to set write deadline")
|
|
stream.Close()
|
|
return
|
|
}
|
|
// Write packet length
|
|
err = binary.Write(stream, binary.LittleEndian, uint16(plen))
|
|
if err != nil {
|
|
stream.Close()
|
|
return
|
|
}
|
|
// Write the packet
|
|
_, err = stream.Write(packet[:plen])
|
|
if err != nil {
|
|
stream.Close()
|
|
return
|
|
}
|
|
|
|
go node.streamHandler(stream)
|
|
}
|
|
|
|
func (node *Node) eventLogger(ctx context.Context, host host.Host) error {
|
|
subCon, err := host.EventBus().Subscribe(new(event.EvtPeerConnectednessChanged))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
node.wg.Done()
|
|
return
|
|
case ev := <-subCon.Out():
|
|
evt := ev.(event.EvtPeerConnectednessChanged)
|
|
for _, vpnPeer := range node.cfg.Peers {
|
|
if vpnPeer.ID == evt.Peer {
|
|
switch evt.Connectedness {
|
|
case network.Connected:
|
|
for _, c := range host.Network().ConnsToPeer(evt.Peer) {
|
|
logger.Info(fmt.Sprintf("Connected to %s/p2p/%s", c.RemoteMultiaddr().String(), evt.Peer.String()))
|
|
}
|
|
case network.NotConnected:
|
|
logger.Info(fmt.Sprintf("Disconnected from %s", evt.Peer.String()))
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
return nil
|
|
}
|
|
|
|
func (node *Node) Rebootstrap() {
|
|
node.p2p.ConnManager().TrimOpenConns(context.Background())
|
|
<-node.dht.ForceRefresh()
|
|
p2p.Rediscover()
|
|
}
|
|
|
|
func (node *Node) Stop() error {
|
|
err := node.p2p.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = os.Remove(node.lockPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
logger.Info("Received signal, shutting down...")
|
|
|
|
err = node.tunDev.Down()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
node.tunDev.Iface.Close()
|
|
node.cancel()
|
|
node.wg.Wait()
|
|
return nil
|
|
}
|
|
|
|
func parseCIDR(s string) net.IPNet {
|
|
_, n, err := net.ParseCIDR(s)
|
|
if err != nil {
|
|
panic("invalid CIDR: " + s)
|
|
}
|
|
return *n
|
|
}
|