Skip to content

Commit

Permalink
Initial version with plain TCP stream
Browse files Browse the repository at this point in the history
  • Loading branch information
sasdf committed Apr 30, 2021
1 parent c7a3ec2 commit ed7ee42
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@

# Dependency directories (remove the comment below to include it)
# vendor/

build/
20 changes: 20 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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/$@
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 |
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/sasdf/nyan

go 1.16
193 changes: 193 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -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 <command> [args...]
Receive file:
nyan recv <file> <port> [host]
Receive data using TCP from host:port and save to <file>.
If host argument is omitted, it will listen on port <port>.
If <file> is "-", data will be written to stdout.
Send file:
nyan send <file> <port> [host]
Send <file> to using TCP to host:port.
If host argument is omitted, it will listen on port <port>.
If <file> 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))
}
}

0 comments on commit ed7ee42

Please sign in to comment.