diff --git a/README.md b/README.md index b720f17..db74942 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Go Report Card](https://goreportcard.com/badge/github.com/hyprspace/hyprspace)](https://goreportcard.com/report/github.com/hyprspace/hyprspace) [![](https://img.shields.io/matrix/hyprspace:matrix.org)](https://matrix.to/#/%23hyprspace:matrix.org) -A Lightweight VPN Built on top of Libp2p for Truly Distributed Networks. +A Lightweight VPN Built on top of IPFS & Libp2p for Truly Distributed Networks. https://user-images.githubusercontent.com/19558067/121469777-f42cdb80-c971-11eb-84de-9dd4f6d6cd1f.mp4 @@ -27,9 +27,9 @@ https://user-images.githubusercontent.com/19558067/121469777-f42cdb80-c971-11eb- **Moreover! Each node doesn't even need to know the other's ip address prior to starting up the connection.** This makes Hyprspace perfect for devices that frequently migrate between locations but still require a constant virtual ip address. ### So How Does Hyprspace Compare to Something Like Wireguard? -[Wireguard](https://wireguard.com) is an amazing VPN written by Jason A. Donenfeld. If you haven't already, definitely go check it out! Wireguard actually inspired me to write Hyprspace. That said, although Wireguard is in a class of its own as a great VPN, it requires at least one of your nodes to have a public IP address. In this mode, as long as one of your nodes is publicly accessible, it can be used as a central relay to reach the other nodes in the network. However, this means that all of the traffic for your entire system is going through that one system which can slow down your network and make it fragile in the case that node goes down and you lose the whole network. So instead say that you want each node to be able to directly connect to each other as they do in Hyprspace. Unfortunately through Wireguard this would require every node to be publicly addressable which means manual port forwarding and no travelling nodes. +[WireGuard](https://wireguard.com) is an amazing VPN written by Jason A. Donenfeld. If you haven't already, definitely go check it out! WireGuard actually inspired me to write Hyprspace. That said, although WireGuard is in a class of its own as a great VPN, it requires at least one of your nodes to have a public IP address. In this mode, as long as one of your nodes is publicly accessible, it can be used as a central relay to reach the other nodes in the network. However, this means that all of the traffic for your entire system is going through that one system which can slow down your network and make it fragile in the case that node goes down and you lose the whole network. So instead say that you want each node to be able to directly connect to each other as they do in Hyprspace. Unfortunately through WireGuard this would require every node to be publicly addressable which means manual port forwarding and no travelling nodes. -By contrast Hyprspace allows all of your nodes to connect directly to each other creating a strong reliable network even if they're all behind their own firewalls. No manual port forwarding required! +By contrast Hyprspace allows all of your nodes to connect directly to each other creating a strong reliable network even if they're all behind their own NATs/firewalls. No manual port forwarding required! ## Use Cases: ##### A Digital Nomad @@ -44,7 +44,9 @@ If anyone else has some use cases please add them! Pull requests welcome! | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ## Getting Started + ### Prerequisites + If you're running Hyprspace on Windows you'll need to install [tap-windows](http://build.openvpn.net/downloads/releases/). ### Installation @@ -175,6 +177,10 @@ and, sudo hyprspace down hs1 ``` +## Disclaimer & Copyright + +WireGuard is a registered trademark of Jason A. Donenfeld. + ## License Copyright 2021-2022 Alec Scott diff --git a/cli/init.go b/cli/init.go index 6e0b01d..31c244d 100644 --- a/cli/init.go +++ b/cli/init.go @@ -1,8 +1,10 @@ package cli import ( + "fmt" "os" "path/filepath" + "strings" "github.com/DataDrake/cli-ng/v2/cmd" "github.com/hyprspace/hyprspace/config" @@ -68,5 +70,17 @@ func InitRun(r *cmd.Root, c *cmd.Sub) { _, err = f.Write(out) checkErr(err) - f.Close() + err = f.Close() + checkErr(err) + + // Print config creation message to user + fmt.Printf("Initialized new config at %s\n", configPath) + fmt.Println("To edit the config run,") + fmt.Println() + if strings.HasPrefix(configPath, "/etc/") { + fmt.Printf(" sudo nano %s\n", configPath) + } else { + fmt.Printf(" nano %s\n", configPath) + } + fmt.Println() } diff --git a/cli/up.go b/cli/up.go index d4d4c9a..c21a0e2 100644 --- a/cli/up.go +++ b/cli/up.go @@ -1,7 +1,6 @@ package cli import ( - "bufio" "context" "encoding/binary" "errors" @@ -24,6 +23,7 @@ import ( "github.com/libp2p/go-libp2p-core/host" "github.com/libp2p/go-libp2p-core/network" "github.com/libp2p/go-libp2p-core/peer" + "github.com/nxadm/tail" ) var ( @@ -76,16 +76,9 @@ func UpRun(r *cmd.Root, c *cmd.Sub) { checkErr(err) if !flags.Foreground { - // Make results chan - out := make(chan error) - go createDaemon(cfg, out) - - select { - case err = <-out: - case <-time.After(30 * time.Second): - } - if err != nil { + if err := createDaemon(cfg); err != nil { fmt.Println("[+] Failed to Create Hyprspace Daemon") + fmt.Println(err) } else { fmt.Println("[+] Successfully Created Hyprspace Daemon") } @@ -156,6 +149,7 @@ func UpRun(r *cmd.Root, c *cmd.Sub) { } fmt.Println("[+] Setting Up Node Discovery via DHT") + // Setup P2P Discovery go p2p.Discover(ctx, host, dht, peerTable) go prettyDiscovery(ctx, host, peerTable) @@ -163,25 +157,8 @@ func UpRun(r *cmd.Root, c *cmd.Sub) { // Configure path for lock lockPath := filepath.Join(filepath.Dir(cfg.Path), cfg.Interface.Name+".lock") - go func() { - // Wait for a SIGINT or SIGTERM signal - ch := make(chan os.Signal, 1) - signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) - <-ch - fmt.Println("Received signal, shutting down...") - - // Shut the node down - if err := host.Close(); err != nil { - panic(err) - } - - // Remove daemon lock from file system. - err = os.Remove(lockPath) - checkErr(err) - - // Exit the application. - os.Exit(0) - }() + // Register the application to listen for SIGINT/SIGTERM + go signalExit(host, lockPath) // Write lock to filesystem to indicate an existing running daemon. err = os.WriteFile(lockPath, []byte(fmt.Sprint(os.Getpid())), os.ModePerm) @@ -194,80 +171,156 @@ func UpRun(r *cmd.Root, c *cmd.Sub) { } fmt.Println("[+] Network Setup Complete...Waiting on Node Discovery") - // Listen For New Packets on TUN Interface + + // + ----------------------------------------+ + // | 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) for { + // Read in a packet from the tun device. plen, err := tunDev.Iface.Read(packet) if err != nil { log.Println(err) continue } + + // Decode the packet's destination address dst := net.IPv4(packet[16], packet[17], packet[18], packet[19]).String() + + // 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 { 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) - ok = false } + + // 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 { continue } + // 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 createDaemon(cfg *config.Config, out chan<- error) { +// singalExit registers two syscall handlers on the system so that if +// an SIGINT or SIGTERM occur on the system hyprspace can gracefully +// shutdown and remove the filesystem lock file. +func signalExit(host host.Host, lockPath string) { + // Wait for a SIGINT or SIGTERM signal + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + <-ch + + // 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...") + + // Exit the application. + os.Exit(0) +} + +// createDaemon handles creating an independent background process for a +// Hyprspace daemon from the original parent process. +func createDaemon(cfg *config.Config) error { path, err := os.Executable() checkErr(err) + + // Generate log path + logPath := filepath.Join(filepath.Dir(cfg.Path), cfg.Interface.Name+".log") + // Create Pipe to monitor for daemon output. - r, w, err := os.Pipe() + f, err := os.Create(logPath) checkErr(err) + // Create Sub Process process, err := os.StartProcess( path, append(os.Args, "--foreground"), &os.ProcAttr{ - Files: []*os.File{nil, w, w}, + Dir: ".", + Env: os.Environ(), + Files: []*os.File{nil, f, f}, }, ) checkErr(err) - scanner := bufio.NewScanner(r) + + // Listen to the child process's log output to determine + // when the daemon is setup and connected to a set of peers. count := 0 - for count < len(cfg.Peers) && scanner.Scan() { - fmt.Println(scanner.Text()) - if strings.HasPrefix(scanner.Text(), "[+] Connection to") { - count++ + deadlineHit := false + countChan := make(chan int) + go func(out chan<- int) { + numConnected := 0 + t, err := tail.TailFile(logPath, tail.Config{Follow: true}) + if err != nil { + out <- numConnected + return } + for line := range t.Lines { + fmt.Println(line.Text) + if strings.HasPrefix(line.Text, "[+] Connection to") { + numConnected++ + } + } + out <- numConnected + }(countChan) + + // Block until all clients are connected or for a maximum of 30s. + select { + case _, deadlineHit = <-time.After(30 * time.Second): + case count = <-countChan: } // Release the created daemon err = process.Release() checkErr(err) - if count < len(cfg.Peers) { - out <- errors.New("failed to create daemon") + + // Check if the daemon exited prematurely + if !deadlineHit && count < len(cfg.Peers) { + return errors.New("failed to create daemon") } - out <- nil + return nil } func streamHandler(stream network.Stream) { @@ -279,12 +332,17 @@ func streamHandler(stream network.Stream) { 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]) @@ -299,6 +357,8 @@ func streamHandler(stream network.Stream) { } func prettyDiscovery(ctx context.Context, node host.Host, peerTable map[string]peer.ID) { + // Build a temporary map of peers to limit querying to only those + // not connected. tempTable := make(map[string]peer.ID, len(peerTable)) for ip, id := range peerTable { tempTable[ip] = id @@ -308,6 +368,7 @@ func prettyDiscovery(ctx context.Context, node host.Host, peerTable map[string]p stream, err := node.NewStream(ctx, id, p2p.Protocol) if err != nil && (strings.HasPrefix(err.Error(), "failed to dial") || strings.HasPrefix(err.Error(), "no addresses")) { + // Attempt to connect to peers slowly when they aren't found. time.Sleep(5 * time.Second) continue } @@ -323,12 +384,17 @@ func prettyDiscovery(ctx context.Context, node host.Host, peerTable map[string]p 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 { diff --git a/config/config.go b/config/config.go index f281f6e..f0450db 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,8 @@ package config import ( + "fmt" + "net" "os" "gopkg.in/yaml.v2" @@ -37,7 +39,7 @@ func Read(path string) (*Config, error) { Interface: Interface{ Name: "hs0", ListenPort: 8001, - Address: "10.1.1.1", + Address: "10.1.1.1/24", ID: "", PrivateKey: "", }, @@ -49,6 +51,13 @@ func Read(path string) (*Config, error) { return nil, err } + // Check peers have valid ip addresses + for ip := range result.Peers { + if net.ParseIP(ip).String() == "" { + return nil, fmt.Errorf("%s is not a valid ip address", ip) + } + } + // Overwrite path of config to input. result.Path = path return &result, nil diff --git a/go.mod b/go.mod index 998d54b..672c2fc 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/libp2p/go-libp2p-quic-transport v0.15.2 github.com/libp2p/go-tcp-transport v0.4.0 github.com/multiformats/go-multiaddr v0.4.1 + github.com/nxadm/tail v1.4.8 github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e github.com/vishvananda/netlink v1.1.0