diff --git a/.gitignore b/.gitignore index 66fd13c..3c964d5 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,5 @@ # Dependency directories (remove the comment below to include it) # vendor/ + +build/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7225bc9 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +#!/bin/env make + + +PROGRAM = nyan + +ARCH = linux_386 linux_amd64 linux_arm linux_arm64 +ARCH += darwin_amd64 darwin_arm64 +ARCH += windows_386.exe windows_amd64.exe + +TARGETS := $(foreach arch,$(ARCH),$(addprefix $(PROGRAM)_,$(arch))) + +all: $(TARGETS) + + +get_os = $(word 2,$(subst _, ,$(word 1,$(subst ., ,$@)))) +get_arch = $(word 3,$(subst _, ,$(word 1,$(subst ., ,$@)))) + +$(TARGETS): %: + @mkdir -p build + CGO_ENABLED=0 GOOS=$(get_os) GOARCH=$(get_arch) go build -ldflags="-s -w" -o build/$@ diff --git a/README.md b/README.md index 94da2da..061d0a4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,48 @@ # nyan Yet another netcat for fast file transfer + +When I need to transfer a file in safe environment (e.g. LAN / VMs), +I just want to use a simple command without cumbersome authentication, GUI, server etc. +`ncat` usually works very well in this case. +However, lastest ncat is super slow on Windows (~32KB/s), and that's why `nyan` was born. +To my surprise, such a naive implementation works very well, even on linux. + +## Features +* Plain TCP stream + * you don't need to use `nyan` at both end +* Progress indicator: Percentage, Size, ETA, Speed + +## Speed +Testing commands: +```sh +Linux ncat: +$ ncat -lvp 1234 --send-only < /dev/zero +$ ncat 127.0.0.1 1234 --recv-only | pv -perb > /dev/null + +Linux nyan: +$ nyan send /dev/zero 1234 +$ nyan recv /dev/null 1234 127.0.0.1 + +Windows: +$ zeros | ncat -lvp 1234 --send-only +$ ncat 127.0.0.1 1234 --recv-only > NUL + +Windows nyan: +$ zeros | nyan send - 1234 +$ nyan recv NUL 1234 127.0.0.1 +``` + +Testing environment: +* Arch Linux host +* Windows 10 20H2 KVM guest with virtio netif connected to local bridge +* Arch Linux KVM guest with virtio netif connected to local bridge +* [Ncat 5.59BETA1](https://nmap.org/ncat/) on Windows performs better than latest version. + +| A | B | nyan (A->B) | ncat (A->B) | nyan (B->A) | ncat (B->A) | +|:----------:|:-----------:|:-----------:|:-----------:|:-----------:|:-----------:| +| Linux Host | localhost | 6.2 GB/s | 2.6 GB/s | - | - | +| Linux VM | localhost | 6.2 GB/s | 1.9 GB/s | - | - | +| Windows VM | localhost | 2.9 GB/s | 0.6 GB/s | - | - | +| Linux Host | Linux VM | 0.8 GB/s | 0.7 GB/s | 5.0 GB/s | 2.2 GB/s | +| Linux Host | Windows VM | 3.2 GB/s | 1.2 GB/s | 2.6 GB/s | 0.3 GB/s | +| Linux VM | Windows VM | 2.5 GB/s | 0.8 GB/s | 0.6 GB/s | 0.3 GB/s | diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..904e1a5 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/sasdf/nyan + +go 1.16 diff --git a/main.go b/main.go new file mode 100644 index 0000000..763ecda --- /dev/null +++ b/main.go @@ -0,0 +1,193 @@ +package main + +import ( + "fmt" + "os" + "math" + "time" + "io" + "strings" + "net" + _ "hash/crc32" +) + + +const bufSize = 1<<20 +const minPrintInterval = 0.1 + +const usageStr = ` +Usage: nyan [args...] + +Receive file: + nyan recv [host] + + Receive data using TCP from host:port and save to . + If host argument is omitted, it will listen on port . + If is "-", data will be written to stdout. + +Send file: + nyan send [host] + + Send to using TCP to host:port. + If host argument is omitted, it will listen on port . + If is "-", data will be read from stdin. +` + +type Pipe interface { + Read(b []byte) (n int, err error) + Write(b []byte) (n int, err error) + Close() error +} + +func fatal(err error) { + fmt.Fprintln(os.Stderr, "[!] Fatal error:", err) + os.Exit(1) +} + +func usage() { + fmt.Fprintln(os.Stderr, strings.TrimSpace(usageStr)) + os.Exit(1) +} + +func connect(port string, host string) net.Conn { + addr := net.JoinHostPort(host, port) + + if host == "" { + ln, err := net.Listen("tcp", addr) + if err != nil { fatal(err) } + defer ln.Close() + + fmt.Fprintln(os.Stderr, "[*] Listening on", addr) + conn, err := ln.Accept() + if err != nil { fatal(err) } + fmt.Fprintln(os.Stderr, "[+] Connection from", conn.RemoteAddr()) + return conn + } else { + conn, err := net.Dial("tcp", addr) + if err != nil { fatal(err) } + fmt.Fprintln(os.Stderr, "[+] Connected to", conn.RemoteAddr()) + return conn + } + +} + +func open(path string, write bool) (file *os.File, err error) { + if path == "-" && write { return os.Stdout, nil } + if path == "-" { return os.Stdin, nil } + if write { return os.Create(path) } + return os.Open(path) +} + +func fmtsi(x float64) string { + for _, u := range " KMGTP" { + if x < 1000 { return fmt.Sprintf("%6.2f%c", x, u) } + x /= 1024 + } + return fmt.Sprintf("%6.2f%s", x, "E") +} + +func fmttime(x_ float64) string { + x := int64(math.Round(x_)) + return fmt.Sprintf("%02d:%02d:%02d", x/3600, x/60%60, x%60) +} + +var lastPrint *time.Time = nil +func progress(cur_ int64, total_ int64, dur_ time.Duration, force bool) { + now := time.Now() + if !force && lastPrint != nil && now.Sub(*lastPrint).Seconds() < minPrintInterval { + return + } + lastPrint = &now + + cur, total, dur := float64(cur_), float64(total_), dur_.Seconds() + if dur < 0.1 { dur = 0.1 } + + speed := cur / dur + + if total_ == 0 { + fmt.Fprintf( + os.Stderr, + "\r[o] %sB [%s] (%sB/s)", + fmtsi(cur), + fmttime(dur), + fmtsi(speed), + ) + } else { + ratio := cur / total + eta := dur / ratio + fmt.Fprintf( + os.Stderr, + "\r[o] %6.2f%% - %sB / %sB [%s / %s] (%sB/s)", + ratio * 100.0, + fmtsi(cur), fmtsi(total), + fmttime(dur), fmttime(eta), + fmtsi(speed), + ) + } +} + +func pipe(src Pipe, dst Pipe, size int64) { + var buf [bufSize]byte + var cur int64 = 0 + // crc32State := crc32.NewIEEE() + start := time.Now() + for { + nbytes, err := src.Read(buf[:]) + if err == io.EOF { break } + if err != nil { fmt.Fprintf(os.Stderr, "\n"); fatal(err) } + _, err = dst.Write(buf[:nbytes]) + if err != nil { fmt.Fprintf(os.Stderr, "\n"); fatal(err) } + // _, err = crc32State.Write(buf[:nbytes]) + // if err != nil { fmt.Fprintf(os.Stderr, "\n"); fatal(err) } + + cur += int64(nbytes) + progress(cur, size, time.Now().Sub(start), false) + } + fmt.Fprintf(os.Stderr, "\n") + // fmt.Fprintf(os.Stderr, "[+] CRC32: %08x\n", crc32State.Sum32()) +} + +func main() { + if len(os.Args) < 4 { + usage() + } + + cmd := os.Args[1] + path := os.Args[2] + port := os.Args[3] + host := ""; if len(os.Args) > 4 { host = os.Args[4] } + + var file *os.File + var err error + var size int64 = 0 + + if cmd == "recv" { + file, err = open(path, true) + } else if cmd == "send" { + file, err = open(path, false) + } else { + fmt.Fprintf(os.Stderr, "Unknown command: %s\n", cmd) + usage() + } + if err != nil { fatal(err) } + defer file.Close() + + info, err := file.Stat() + if err != nil { fatal(err) } + size = info.Size() + + if size > 0 { + fmt.Fprintf(os.Stderr, "[+] File Size: %sB\n", fmtsi(float64(size))) + } + + conn := connect(port, host) + defer conn.Close() + + if cmd == "send" { + pipe(file, conn, size) + } else if cmd == "recv" { + pipe(conn, file, size) + } else { + fatal(fmt.Errorf("WTF: Unknown command: %s", cmd)) + } +}