depot/packages/networking/hyprspace/cli/up.go

397 lines
10 KiB
Go

package cli
import (
"context"
"encoding/binary"
"errors"
"fmt"
"io/fs"
"net"
"os"
"os/signal"
"path/filepath"
"runtime"
"strconv"
"syscall"
"time"
"github.com/DataDrake/cli-ng/v2/cmd"
"github.com/hyprspace/hyprspace/config"
"github.com/hyprspace/hyprspace/p2p"
hsrpc "github.com/hyprspace/hyprspace/rpc"
"github.com/hyprspace/hyprspace/tun"
dht "github.com/libp2p/go-libp2p-kad-dht"
"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/multiformats/go-multibase"
)
var (
// iface is the tun device used to pass packets between
// Hyprspace and the user's machine.
tunDev *tun.TUN
// RevLookup allow quick lookups of an incoming stream
// for security before accepting or responding to any data.
RevLookup map[string]string
// activeStreams is a map of active streams to a peer
activeStreams map[string]network.Stream
// context
ctx context.Context
// context cancel function
ctxCancel func()
)
// Up creates and brings up a Hyprspace Interface.
var Up = cmd.Sub{
Name: "up",
Alias: "up",
Short: "Create and Bring Up a Hyprspace Interface.",
Args: &UpArgs{},
Flags: &UpFlags{},
Run: UpRun,
}
// UpArgs handles the specific arguments for the up command.
type UpArgs struct {
InterfaceName string
}
// UpFlags handles the specific flags for the up command.
type UpFlags struct {
Foreground bool `short:"f" long:"foreground" desc:"Don't Create Background Daemon."`
}
// UpRun handles the execution of the up command.
func UpRun(r *cmd.Root, c *cmd.Sub) {
// Parse Command Args
args := c.Args.(*UpArgs)
// Parse Global Config Flag for Custom Config Path
configPath := r.Flags.(*GlobalFlags).Config
if configPath == "" {
configPath = "/etc/hyprspace/" + args.InterfaceName + ".yaml"
}
// Read in configuration from file.
cfg, err := config.Read(configPath)
checkErr(err)
// Setup reverse lookup hash map for authentication.
RevLookup = make(map[string]string, len(cfg.Peers))
for ip, id := range cfg.Peers {
RevLookup[id.ID] = ip
}
fmt.Println("[+] Creating TUN Device")
if runtime.GOOS == "darwin" {
if len(cfg.Peers) > 1 {
checkErr(errors.New("cannot create interface macos does not support more than one peer"))
}
// Grab ip address of only peer in config
var destPeer string
for ip := range cfg.Peers {
destPeer = ip
}
// Create new TUN device
tunDev, err = tun.New(
cfg.Interface.Name,
tun.Address(cfg.Interface.Address),
tun.DestAddress(destPeer),
tun.MTU(1420),
)
} else {
// Create new TUN device
tunDev, err = tun.New(
cfg.Interface.Name,
tun.Address(cfg.Interface.Address),
tun.MTU(1420),
)
}
if err != nil {
checkErr(err)
}
// Setup System Context
ctx, ctxCancel = context.WithCancel(context.Background())
fmt.Println("[+] Creating LibP2P Node")
// Check that the listener port is available.
port, err := verifyPort(cfg.Interface.ListenPort)
checkErr(err)
_, privateKey, err := multibase.Decode(cfg.Interface.PrivateKey)
// Create P2P Node
host, dht, err := p2p.CreateNode(
ctx,
privateKey,
port,
streamHandler,
)
checkErr(err)
host.SetStreamHandler(p2p.PeXProtocol, p2p.NewPeXStreamHandler(host, cfg))
for _, id := range cfg.Peers {
p, err := peer.Decode(id.ID)
checkErr(err)
host.ConnManager().Protect(p, "/hyprspace/peer")
}
// Setup Peer Table for Quick Packet --> Dest ID lookup
peerTable := make(map[string]peer.ID)
for ip, id := range cfg.Peers {
peerTable[ip], err = peer.Decode(id.ID)
checkErr(err)
}
fmt.Println("[+] Setting Up Node Discovery via DHT")
// Setup P2P Discovery
go p2p.Discover(ctx, host, dht, peerTable)
// Configure path for lock
lockPath := filepath.Join(filepath.Dir(cfg.Path), cfg.Interface.Name+".lock")
// PeX
go p2p.PeXService(ctx, host, cfg)
// Register the application to listen for signals
go signalHandler(ctx, host, lockPath, dht)
// Log about various events
go eventLogger(ctx, host, cfg)
// RPC server
go hsrpc.RpcServer(ctx, multiaddr.StringCast(fmt.Sprintf("/unix/run/hyprspace-rpc.%s.sock", cfg.Interface.Name)), host, *cfg)
// Write lock to filesystem to indicate an existing running daemon.
err = os.WriteFile(lockPath, []byte(fmt.Sprint(os.Getpid())), os.ModePerm)
checkErr(err)
// Bring Up TUN Device
err = tunDev.Up()
if err != nil {
checkErr(errors.New("unable to bring up tun device"))
}
fmt.Println("[+] Network setup complete")
// + ----------------------------------------+
// | Listen For New Packets on TUN Interface |
// + ----------------------------------------+
// Initialize active streams map and packet byte array.
activeStreams = make(map[string]network.Stream)
var packet = make([]byte, 1420)
ip, _, err := net.ParseCIDR(cfg.Interface.Address)
if err != nil {
checkErr(errors.New("unable to parse address"))
}
for {
// Read in a packet from the tun device.
plen, err := tunDev.Iface.Read(packet)
if errors.Is(err, fs.ErrClosed) {
fmt.Println("[-] Interface closed")
<-ctx.Done()
time.Sleep(1 * time.Second)
return
} else if err != nil {
fmt.Println(err)
continue
}
dstIP := net.IPv4(packet[16], packet[17], packet[18], packet[19])
dst := dstIP.String()
// Check route table for destination address.
for route, _ := range cfg.Routes {
_, network, _ := net.ParseCIDR(route)
if network.Contains(dstIP) {
src := net.IPv4(packet[12], packet[13], packet[14], packet[15])
_, ok := peerTable[dst]
// Only rewrite if initiator is us or receiver is not a known peer
if src.Equal(ip) && !ok {
dst = cfg.Routes[route].IP
}
}
}
// Check if we already have an open connection to the destination peer.
stream, ok := activeStreams[dst]
if ok {
// 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(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 = stream.Write(packet[:plen])
if err == nil {
stream.SetWriteDeadline(time.Now().Add(25 * time.Second))
continue
}
}
// If we encounter an error when writing to a stream we should
// close that stream and delete it from the active stream map.
stream.Close()
delete(activeStreams, dst)
}
// Check if the destination of the packet is a known peer to
// the interface.
if peer, ok := peerTable[dst]; ok {
stream, err = host.NewStream(ctx, peer, p2p.Protocol)
if err != nil {
fmt.Println("[!] Failed to open stream to " + dst)
go p2p.Rediscover()
continue
}
stream.SetWriteDeadline(time.Now().Add(25 * time.Second))
// Write packet length
err = binary.Write(stream, binary.LittleEndian, uint16(plen))
if err != nil {
stream.Close()
continue
}
// Write the packet
_, err = stream.Write(packet[:plen])
if err != nil {
stream.Close()
continue
}
// If all succeeds when writing the packet to the stream
// we should reuse this stream by adding it active streams map.
activeStreams[dst] = stream
}
}
}
func signalHandler(ctx context.Context, host host.Host, lockPath string, dht *dht.IpfsDHT) {
exitCh := make(chan os.Signal, 1)
rebootstrapCh := make(chan os.Signal, 1)
signal.Notify(exitCh, syscall.SIGINT, syscall.SIGTERM)
signal.Notify(rebootstrapCh, syscall.SIGUSR1)
for {
select {
case <-ctx.Done():
return
case <-rebootstrapCh:
fmt.Println("[-] Rebootstrapping on SIGUSR1")
host.ConnManager().TrimOpenConns(context.Background())
<-dht.ForceRefresh()
p2p.Rediscover()
case <-exitCh:
// Shut the node down
err := host.Close()
checkErr(err)
// Remove daemon lock from file system.
err = os.Remove(lockPath)
checkErr(err)
fmt.Println("Received signal, shutting down...")
tunDev.Iface.Close()
tunDev.Down()
ctxCancel()
}
}
}
func eventLogger(ctx context.Context, host host.Host, cfg *config.Config) {
subCon, err := host.EventBus().Subscribe(new(event.EvtPeerConnectednessChanged))
checkErr(err)
for {
select {
case <-ctx.Done():
return
case ev := <-subCon.Out():
evt := ev.(event.EvtPeerConnectednessChanged)
for vpnIp, vpnPeer := range cfg.Peers {
if vpnPeer.ID == evt.Peer.Pretty() {
if evt.Connectedness == network.Connected {
for _, c := range host.Network().ConnsToPeer(evt.Peer) {
fmt.Printf("[+] Connected to %s at %s/p2p/%s\n", vpnIp, c.RemoteMultiaddr().String(), evt.Peer.Pretty())
}
} else if evt.Connectedness == network.NotConnected {
fmt.Printf("[!] Disconnected from %s\n", vpnIp)
}
break
}
}
}
}
}
func streamHandler(stream network.Stream) {
// If the remote node ID isn't in the list of known nodes don't respond.
if _, ok := RevLookup[stream.Conn().RemotePeer().Pretty()]; !ok {
stream.Reset()
return
}
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
}
}
stream.SetWriteDeadline(time.Now().Add(25 * time.Second))
tunDev.Iface.Write(packet[:size])
}
}
func verifyPort(port int) (int, error) {
var ln net.Listener
var err error
// If a user manually sets a port don't try to automatically
// find an open port.
if port != 8001 {
ln, err = net.Listen("tcp", ":"+strconv.Itoa(port))
if err != nil {
return port, errors.New("could not create node, listen port already in use by something else")
}
} else {
// Automatically look for an open port when a custom port isn't
// selected by a user.
for {
ln, err = net.Listen("tcp", ":"+strconv.Itoa(port))
if err == nil {
break
}
if port >= 65535 {
return port, errors.New("failed to find open port")
}
port++
}
}
if ln != nil {
ln.Close()
}
return port, nil
}