├── .github ├── img │ ├── banner.png │ ├── cloud-shell.png │ └── windows-rdp-demo.mov └── workflows │ ├── release.yml │ └── main.yml ├── Dockerfile ├── .gitignore ├── go.mod ├── LICENSE ├── main.go ├── utils └── utils.go ├── pkg ├── exec_win.go ├── session.go ├── exec_android.go ├── exec_unix.go └── netcat.go ├── go.sum ├── Makefile ├── log └── log.go ├── mkrelease.sh ├── config └── options.go └── README.md /.github/img/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsocket/qs-netcat/HEAD/.github/img/banner.png -------------------------------------------------------------------------------- /.github/img/cloud-shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsocket/qs-netcat/HEAD/.github/img/cloud-shell.png -------------------------------------------------------------------------------- /.github/img/windows-rdp-demo.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qsocket/qs-netcat/HEAD/.github/img/windows-rdp-demo.mov -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest as builder 2 | RUN go install github.com/qsocket/qs-netcat@master 3 | ENTRYPOINT ["qs-netcat"] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Build directory 9 | build/ 10 | release/ 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Keys and certificates 19 | *.key 20 | *.pem 21 | 22 | # Dependency directories (remove the comment below to include it) 23 | vendor/ 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | 2 | name: release 3 | 4 | on: 5 | workflow_dispatch: 6 | branches: [ master ] 7 | jobs: 8 | release-build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Set up Go 13 | uses: actions/setup-go@v3 14 | with: 15 | go-version: 1.21 16 | - name: Make All Targets 17 | run: make all 18 | - name: Package Builds 19 | - run: ./mkrelease.sh 20 | - name: 'Upload Artifact' 21 | uses: actions/upload-artifact@v3 22 | with: 23 | name: qs-netcat_all_builds 24 | path: ./release/* 25 | retention-days: 5 26 | 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/qsocket/qs-netcat 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/alecthomas/kong v0.7.1 7 | github.com/briandowns/spinner v1.23.0 8 | github.com/creack/pty v1.1.18 9 | github.com/fatih/color v1.15.0 10 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 11 | github.com/mdp/qrterminal/v3 v3.1.1 12 | github.com/qsocket/conpty-go v0.0.0-20230315180542-d8f8596877dc 13 | github.com/qsocket/qsocket-go v0.0.5-beta.0.20240801201213-a1b08f6321d3 14 | golang.org/x/sys v0.22.0 15 | golang.org/x/term v0.22.0 16 | ) 17 | 18 | require ( 19 | github.com/mattn/go-colorable v0.1.13 // indirect 20 | github.com/mattn/go-isatty v0.0.20 // indirect 21 | github.com/qsocket/encrypted-stream v0.0.0-20231023165659-580d263e71f4 // indirect 22 | github.com/qsocket/go-srp v0.0.0-20230315175014-fb16dd9247df // indirect 23 | golang.org/x/crypto v0.25.0 // indirect 24 | golang.org/x/net v0.27.0 // indirect 25 | rsc.io/qr v0.2.0 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Qsocket 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "time" 7 | 8 | "github.com/qsocket/qs-netcat/config" 9 | "github.com/qsocket/qs-netcat/log" 10 | qsnetcat "github.com/qsocket/qs-netcat/pkg" 11 | "github.com/qsocket/qs-netcat/utils" 12 | "github.com/qsocket/qsocket-go" 13 | ) 14 | 15 | func main() { 16 | // Configure the options from the flags/config file 17 | opts, err := config.ConfigureOptions() 18 | if err != nil || opts == nil { 19 | log.Fatal(err) 20 | return 21 | } 22 | if opts.RandomSecret { 23 | opts.Secret = utils.RandomString(20) 24 | } 25 | opts.Summarize() 26 | 27 | if opts.Listen { 28 | go utils.WaitForExitSignal(os.Interrupt) 29 | firstRun := true 30 | log.Info("Listening for connections...") 31 | for { 32 | if !firstRun { 33 | time.Sleep(time.Duration(opts.ProbeInterval) * time.Second) 34 | } else { 35 | firstRun = false 36 | } 37 | err := qsnetcat.ProbeQSRN(opts) 38 | if err != nil { 39 | switch err { 40 | case qsocket.ErrPeerNotFound, io.EOF: 41 | log.Debug(err) 42 | case qsocket.ErrServerCollision: 43 | log.Fatal(err) 44 | default: 45 | log.Error(err) 46 | } 47 | } 48 | } 49 | 50 | } 51 | 52 | err = qsnetcat.Connect(opts) 53 | if err != nil && err != io.EOF { 54 | log.Error(err) 55 | os.Exit(1) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | 2 | name: build 3 | 4 | on: 5 | push: 6 | branches: [ "master" ] 7 | 8 | jobs: 9 | build-all: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Go 14 | uses: actions/setup-go@v3 15 | with: 16 | go-version: 1.21 17 | - name: Make All Targets 18 | run: make all 19 | - name: 'Upload Artifact' 20 | uses: actions/upload-artifact@v3 21 | with: 22 | name: qs-netcat_all_builds 23 | path: ./build/* 24 | retention-days: 5 25 | macos-run: 26 | runs-on: macos-latest 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Set up Go 30 | uses: actions/setup-go@v3 31 | with: 32 | go-version: 1.21 33 | - name: Make Build 34 | run: make 35 | - name: Test Run 36 | run: ./build/qs-netcat -g 37 | linux-run: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v3 41 | - name: Set up Go 42 | uses: actions/setup-go@v3 43 | with: 44 | go-version: 1.21 45 | - name: Make Build 46 | run: make 47 | - name: Test Run 48 | run: ./build/qs-netcat -g 49 | windows-run: 50 | runs-on: windows-latest 51 | steps: 52 | - uses: actions/checkout@v3 53 | - name: Set up Go 54 | uses: actions/setup-go@v3 55 | with: 56 | go-version: 1.21 57 | - name: Make Build 58 | run: make 59 | - name: Test Run 60 | run: ./build/qs-netcat.exe -g 61 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "github.com/fatih/color" 11 | "github.com/qsocket/qs-netcat/log" 12 | ) 13 | 14 | var ( 15 | Red = color.New(color.FgRed) 16 | Blue = color.New(color.FgBlue) 17 | Yellow = color.New(color.FgYellow) 18 | BoldRed = color.New(color.FgRed).Add(color.Bold) 19 | BoldBlue = color.New(color.FgBlue).Add(color.Bold) 20 | BoldGreen = color.New(color.FgGreen).Add(color.Bold) 21 | BoldYellow = color.New(color.FgYellow).Add(color.Bold) 22 | ) 23 | 24 | func EnableSmartPipe() { 25 | color.NoColor = false 26 | os.Stdout = os.NewFile(uintptr(syscall.Stderr), "stderr") 27 | } 28 | 29 | func IsFilePiped(f *os.File) bool { 30 | fs, err := f.Stat() 31 | if err != nil { 32 | log.Error(err) 33 | } 34 | return (fs.Mode() & os.ModeCharDevice) == 0 35 | } 36 | 37 | func PrintFatal(format string, a ...any) { 38 | fmt.Printf("%s ", Red.Sprintf("[!]")) 39 | fmt.Printf(format, a...) 40 | } 41 | 42 | func PrintStatus(format string, a ...any) { 43 | fmt.Printf("%s ", Yellow.Sprintf("[*]")) 44 | fmt.Printf(format, a...) 45 | } 46 | 47 | func CaclChecksum(data []byte, base uint) uint { 48 | checksum := uint(0) 49 | for _, n := range data { 50 | checksum += uint(n) 51 | } 52 | return checksum % base 53 | } 54 | 55 | func RandomString(n int) string { 56 | // rand.Seed(time.Now().UTC().UnixMicro()) 57 | letterRunes := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") 58 | b := make([]rune, n) 59 | for i := range b { 60 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 61 | } 62 | return string(b) 63 | } 64 | 65 | func WaitForExitSignal(sig os.Signal) { 66 | for { 67 | sigChan := make(chan os.Signal, 1) 68 | signal.Notify(sigChan, sig) 69 | <-sigChan 70 | print("\n") 71 | PrintFatal("Exiting...\n") 72 | os.Exit(0) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /pkg/exec_win.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package qsnetcat 5 | 6 | import ( 7 | "context" 8 | "io" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "syscall" 13 | 14 | "github.com/google/shlex" 15 | conpty "github.com/qsocket/conpty-go" 16 | "github.com/qsocket/qs-netcat/log" 17 | qsocket "github.com/qsocket/qsocket-go" 18 | ) 19 | 20 | // const ATTACH_PARENT_PROCESS = ^uintptr(0) 21 | const SHELL = "cmd.exe" 22 | 23 | // func init() { 24 | // proc := syscall.MustLoadDLL("KERNEL32.dll").MustFindProc("AttachConsole") 25 | // proc.Call(ATTACH_PARENT_PROCESS) // We need this to get console output when using windowsgui subsystem. 26 | // } 27 | 28 | func ExecCommand(conn *qsocket.QSocket, specs SessionSpecs) error { 29 | // If non specified spawn OS shell... 30 | if specs.Command == "" { 31 | specs.Command = SHELL 32 | } 33 | 34 | defer conn.Close() 35 | params, err := shlex.Split(specs.Command) 36 | if err != nil { 37 | return err 38 | } 39 | ncDir, err := filepath.Abs(os.Args[0]) // Get the full path of the executalbe. 40 | if err != nil { 41 | return err 42 | } 43 | os.Setenv("qs_netcat", ncDir) // Set binary dir to env variable for easy access. 44 | cmd := exec.Command(params[0]) 45 | if len(params) > 1 { 46 | cmd = exec.Command(params[0], params[1:]...) 47 | } 48 | cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} // Hide new process window 49 | if specs.Interactive { 50 | cpty, err := conpty.Start(specs.Command) 51 | if err != nil { 52 | return err 53 | } 54 | defer cpty.Close() 55 | err = cpty.Resize(int(specs.TermSize.Cols), int(specs.TermSize.Rows)) 56 | if err != nil { 57 | log.Error(err) 58 | } 59 | 60 | go func() { 61 | go io.Copy(conn, cpty) 62 | io.Copy(cpty, conn) 63 | }() 64 | 65 | _, err = cpty.Wait(context.Background()) 66 | return err 67 | } 68 | 69 | cmd.Stdin = conn 70 | cmd.Stdout = conn 71 | cmd.Stderr = conn 72 | return cmd.Run() 73 | } 74 | 75 | func GetCurrentTermSize() (*Winsize, error) { 76 | return &Winsize{Cols: 80, Rows: 40}, nil 77 | } 78 | -------------------------------------------------------------------------------- /pkg/session.go: -------------------------------------------------------------------------------- 1 | package qsnetcat 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | 7 | "github.com/qsocket/qs-netcat/config" 8 | "github.com/qsocket/qsocket-go" 9 | ) 10 | 11 | type SessionSpecs struct { 12 | Command string 13 | ForwardAddr string 14 | TermSize Winsize 15 | Interactive bool 16 | } 17 | 18 | // Winsize describes the terminal window size 19 | type Winsize struct { 20 | Rows uint16 // ws_row: Number of rows (in cells) 21 | Cols uint16 // ws_col: Number of columns (in cells) 22 | X uint16 // ws_xpixel: Width in pixels 23 | Y uint16 // ws_ypixel: Height in pixels 24 | } 25 | 26 | func SendSessionSpecs(qs *qsocket.QSocket, opts *config.Options) error { 27 | if qs.IsClosed() { 28 | return qsocket.ErrSocketNotConnected 29 | } 30 | 31 | ws := new(Winsize) 32 | err := error(nil) 33 | if !opts.IsPiped() { 34 | ws, err = GetCurrentTermSize() 35 | if err != nil { 36 | return err 37 | } 38 | } 39 | 40 | specs := SessionSpecs{ 41 | Command: opts.Execute, 42 | ForwardAddr: opts.ForwardAddr, 43 | TermSize: Winsize{ 44 | Cols: ws.Cols, 45 | Rows: ws.Rows, 46 | X: ws.X, 47 | Y: ws.Y, 48 | }, 49 | Interactive: opts.Interactive, 50 | } 51 | buf := bytes.Buffer{} 52 | enc := gob.NewEncoder(&buf) 53 | 54 | if err := enc.Encode(specs); err != nil { 55 | return err 56 | } 57 | 58 | _, err = qs.Write(buf.Bytes()) 59 | return err 60 | } 61 | 62 | func RecvSessionSpecs(qs *qsocket.QSocket, opts *config.Options) (*SessionSpecs, error) { 63 | if qs.IsClosed() { 64 | return nil, qsocket.ErrSocketNotConnected 65 | } 66 | specs := new(SessionSpecs) 67 | data := make([]byte, 512) 68 | n, err := qs.Read(data) 69 | if err != nil { 70 | return nil, err 71 | } 72 | buf := bytes.NewBuffer(data[:n]) 73 | dec := gob.NewDecoder(buf) 74 | 75 | if err := dec.Decode(specs); err != nil { 76 | return nil, err 77 | } 78 | 79 | if specs.TermSize.Cols == 0 && 80 | specs.TermSize.Rows == 0 && 81 | !opts.IsPiped() { 82 | ws, err := GetCurrentTermSize() 83 | if err != nil { 84 | return nil, err 85 | } 86 | specs.TermSize = Winsize{ 87 | Cols: ws.Cols, 88 | Rows: ws.Rows, 89 | } 90 | } 91 | 92 | if specs.Command == "" { 93 | specs.Command = opts.Execute 94 | } 95 | 96 | return specs, nil 97 | } 98 | -------------------------------------------------------------------------------- /pkg/exec_android.go: -------------------------------------------------------------------------------- 1 | //go:build android 2 | // +build android 3 | 4 | package qsnetcat 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "os" 10 | "os/exec" 11 | "syscall" 12 | "unsafe" 13 | 14 | "github.com/creack/pty" 15 | "github.com/google/shlex" 16 | "github.com/qsocket/qs-netcat/log" 17 | qsocket "github.com/qsocket/qsocket-go" 18 | // _ "golang.org/x/mobile/app" 19 | ) 20 | 21 | const SHELL = "sh" 22 | 23 | var ( 24 | PtyHeight int = 39 25 | PtyWidth int = 157 26 | ) 27 | 28 | func ExecCommand(conn *qsocket.QSocket, specs SessionSpecs) error { 29 | // If non specified spawn OS shell... 30 | if specs.Command == "" { 31 | specs.Command = SHELL 32 | } 33 | 34 | defer conn.Close() 35 | params, err := shlex.Split(specs.Command) 36 | if err != nil { 37 | return err 38 | } 39 | ncDir, err := os.Executable() // Get the full path of the executalbe. 40 | if err != nil { 41 | return err 42 | } 43 | os.Setenv("qs_netcat", ncDir) // Set binary dir to env variable for easy access. 44 | os.Setenv("HISTFILE", "/dev/null") // Unset histfile for disabling logging. 45 | cmd := exec.Command(params[0]) 46 | if len(params) > 1 { 47 | cmd = exec.Command(params[0], params[1:]...) 48 | } 49 | 50 | if specs.Interactive { 51 | 52 | // Start the command with a pty. 53 | ptmx, err := pty.StartWithSize( 54 | cmd, 55 | &pty.Winsize{Cols: specs.TermSize.Cols, Rows: specs.TermSize.Rows}, 56 | ) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | // Make sure to close the pty at the end. 62 | defer ptmx.Close() // Best effort. 63 | 64 | // Copy stdin to the pty and the pty to stdout. 65 | // NOTE: The goroutine will keep reading until the next keystroke before returning. 66 | go io.Copy(ptmx, conn) 67 | io.Copy(conn, ptmx) 68 | return nil 69 | } else { 70 | // Handle pty size. 71 | err = pty.Setsize(os.Stdin, &pty.Winsize{Rows: specs.TermSize.Rows, Cols: specs.TermSize.Cols}) 72 | if err != nil { 73 | log.Error(err) 74 | } 75 | } 76 | 77 | cmd.Stdin = conn 78 | cmd.Stdout = conn 79 | cmd.Stderr = conn 80 | return cmd.Run() 81 | } 82 | 83 | func GetCurrentTermSize() (*Winsize, error) { 84 | ws := &Winsize{} 85 | retCode, _, errno := syscall.Syscall(syscall.SYS_IOCTL, 86 | uintptr(syscall.Stdin), 87 | uintptr(syscall.TIOCGWINSZ), 88 | uintptr(unsafe.Pointer(ws))) 89 | 90 | if int(retCode) == -1 { 91 | return nil, fmt.Errorf("TIOCGWINSZ syscall failed with %d!", errno) 92 | } 93 | return ws, nil 94 | } 95 | -------------------------------------------------------------------------------- /pkg/exec_unix.go: -------------------------------------------------------------------------------- 1 | //go:build !windows && !android 2 | // +build !windows,!android 3 | 4 | package qsnetcat 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "io" 10 | "os" 11 | "os/exec" 12 | "sync/atomic" 13 | 14 | // "syscall" 15 | "unsafe" 16 | 17 | "github.com/creack/pty" 18 | "github.com/google/shlex" 19 | "github.com/qsocket/qs-netcat/log" 20 | qsocket "github.com/qsocket/qsocket-go" 21 | "golang.org/x/sys/unix" 22 | ) 23 | 24 | const SHELL = "/bin/bash -il" 25 | 26 | var ( 27 | PtyHeight int = 39 28 | PtyWidth int = 157 29 | RawPty atomic.Bool 30 | ErrRawPtyInUse = errors.New("Raw PTY is already in use.") 31 | ) 32 | 33 | func ExecCommand(conn *qsocket.QSocket, specs SessionSpecs) error { 34 | // If non specified spawn OS shell... 35 | if specs.Command == "" { 36 | specs.Command = SHELL 37 | } 38 | 39 | defer conn.Close() 40 | params, err := shlex.Split(specs.Command) 41 | if err != nil { 42 | return err 43 | } 44 | ncDir, err := os.Executable() // Get the full path of the executalbe. 45 | if err != nil { 46 | return err 47 | } 48 | os.Setenv("qs_netcat", ncDir) // Set binary dir to env variable for easy access. 49 | os.Setenv("HISTFILE", "/dev/null") // Unset histfile for disabling logging. 50 | cmd := exec.Command(params[0]) 51 | if cmd == nil { 52 | return errors.New("exec.Command returned nil") 53 | } 54 | if len(params) > 1 { 55 | cmd = exec.Command(params[0], params[1:]...) 56 | } 57 | 58 | if !specs.Interactive && RawPty.Load() { 59 | return ErrRawPtyInUse 60 | } 61 | 62 | if specs.Interactive { 63 | // Start the command with a pty. 64 | ptmx, err := pty.StartWithSize( 65 | cmd, 66 | &pty.Winsize{Cols: specs.TermSize.Cols, Rows: specs.TermSize.Rows}, 67 | ) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | // Make sure to close the pty at the end. 73 | defer ptmx.Close() // Best effort. 74 | 75 | // Copy stdin to the pty and the pty to stdout. 76 | // NOTE: The goroutine will keep reading until the next keystroke before returning. 77 | go io.Copy(ptmx, conn) 78 | io.Copy(conn, ptmx) 79 | return nil 80 | } else { 81 | // Handle pty size. 82 | err = pty.Setsize(os.Stdin, &pty.Winsize{Rows: specs.TermSize.Rows, Cols: specs.TermSize.Cols}) 83 | if err != nil { 84 | log.Error(err) 85 | } 86 | } 87 | 88 | RawPty.Store(true) 89 | cmd.Stdin = conn 90 | cmd.Stdout = conn 91 | cmd.Stderr = conn 92 | return cmd.Run() 93 | } 94 | 95 | func GetCurrentTermSize() (*Winsize, error) { 96 | ws := &Winsize{} 97 | retCode, _, errno := unix.Syscall(unix.SYS_IOCTL, 98 | uintptr(unix.Stdin), 99 | uintptr(unix.TIOCGWINSZ), 100 | uintptr(unsafe.Pointer(ws))) 101 | 102 | if int(retCode) == -1 { 103 | return nil, fmt.Errorf("TIOCGWINSZ syscall failed with %d!", errno) 104 | } 105 | return ws, nil 106 | } 107 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0= 2 | github.com/alecthomas/kong v0.7.1 h1:azoTh0IOfwlAX3qN9sHWTxACE2oV8Bg2gAwBsMwDQY4= 3 | github.com/alecthomas/kong v0.7.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= 4 | github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= 5 | github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A= 6 | github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE= 7 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 8 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 9 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 10 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 11 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 12 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 13 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 14 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 15 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 16 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 17 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 18 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 19 | github.com/mdp/qrterminal/v3 v3.1.1 h1:cIPwg3QU0OIm9+ce/lRfWXhPwEjOSKwk3HBwL3HBTyc= 20 | github.com/mdp/qrterminal/v3 v3.1.1/go.mod h1:5lJlXe7Jdr8wlPDdcsJttv1/knsRgzXASyr4dcGZqNU= 21 | github.com/qsocket/conpty-go v0.0.0-20230315180542-d8f8596877dc h1:XSfMXQ75WkkiCA7lBOBaq2dr0+9+KaqmM/QvGfLIzx0= 22 | github.com/qsocket/conpty-go v0.0.0-20230315180542-d8f8596877dc/go.mod h1:CjcNvNYhzkvf4UhGSgsUXgUzh+j/gyXNeTto6Coz0Gc= 23 | github.com/qsocket/encrypted-stream v0.0.0-20231023165659-580d263e71f4 h1:EpdKbGFjc8VPtA8CTwV/F+kqtbuOyqFTky7RipOfcB8= 24 | github.com/qsocket/encrypted-stream v0.0.0-20231023165659-580d263e71f4/go.mod h1:ev4+HY9osvIdxmY0ZJ78z4QjF2xMz/r0OTEgl30+mV0= 25 | github.com/qsocket/go-srp v0.0.0-20230315175014-fb16dd9247df h1:PbAZ0Eb2pfUZ/tyNpLq45aYb28VH59EIRKE1zBQRu0g= 26 | github.com/qsocket/go-srp v0.0.0-20230315175014-fb16dd9247df/go.mod h1:qTi2TUfUFeM6K/d2DGDuI4s2eL+cvHGKOsHvIqxXujY= 27 | github.com/qsocket/qsocket-go v0.0.5-beta.0.20240801201213-a1b08f6321d3 h1:hk7+o2Qp+QyIkbhaqX5VuihGm3e+PLCg+oo2jiAUB+c= 28 | github.com/qsocket/qsocket-go v0.0.5-beta.0.20240801201213-a1b08f6321d3/go.mod h1:L311tY5zuH/whw0lxiAoABbvViMMhoLVPld4xEIvr90= 29 | github.com/qsocket/qsocket-go v0.0.8-beta h1:SHpeksJFhFkwlE8+Lpy+UdGZaHDsdT3nhJv7Da2xPgo= 30 | github.com/qsocket/qsocket-go v0.0.8-beta/go.mod h1:L311tY5zuH/whw0lxiAoABbvViMMhoLVPld4xEIvr90= 31 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 32 | golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 33 | golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= 34 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 35 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 36 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 37 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 38 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 39 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 43 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 44 | golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= 45 | golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 46 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 47 | rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= 48 | rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= 49 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CURRET_DIR=$(shell pwd) 2 | BUILD=CGO_ENABLED=0 go build 3 | OUT_DIR=${CURRET_DIR}/build 4 | BUILD_FLAGS=-trimpath -buildvcs=false -ldflags="-extldflags=-static -s -w -X github.com/qsocket/qs-netcat/config.Version=$$(git describe --tags)" 5 | WIN_BUILD_FLAGS=-trimpath -buildvcs=false -ldflags="-H windowsgui -s -w -X github.com/qsocket/qs-netcat/config.Version=$$(git describe --tags)" 6 | ANDROID_API=33 7 | ANDROID_BUILD=CGO_ENABLED=1 go build -trimpath -buildvcs=false -ldflags="-s -w -X github.com/qsocket/qs-netcat/config.Version=$$(git describe --tags)" 8 | $(shell mkdir -p build/{windows,linux,darwin,android,ios,freebsd,openbsd,solaris,aix,illumos,dragonfly}) 9 | 10 | default: 11 | ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/ 12 | windows: 13 | GOOS=windows GOARCH=amd64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/windows/qs-netcat-amd64.exe 14 | GOOS=windows GOARCH=386 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/windows/qs-netcat-386.exe 15 | GOOS=windows GOARCH=arm ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/windows/qs-netcat-arm.exe 16 | GOOS=windows GOARCH=arm64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/windows/qs-netcat-arm64.exe 17 | linux: 18 | GOOS=linux GOARCH=amd64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/linux/qs-netcat-amd64 19 | GOOS=linux GOARCH=386 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/linux/qs-netcat-386 20 | GOOS=linux GOARCH=arm ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/linux/qs-netcat-arm 21 | GOOS=linux GOARCH=arm64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/linux/qs-netcat-arm64 22 | GOOS=linux GOARCH=mips ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/linux/qs-netcat-mips 23 | GOOS=linux GOARCH=mips64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/linux/qs-netcat-mips64 24 | GOOS=linux GOARCH=mips64le ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/linux/qs-netcat-mips64le 25 | GOOS=linux GOARCH=mipsle ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/linux/qs-netcat-mipsle 26 | GOOS=linux GOARCH=ppc64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/linux/qs-netcat-ppc64 27 | GOOS=linux GOARCH=ppc64le ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/linux/qs-netcat-ppc64le 28 | GOOS=linux GOARCH=s390x ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/linux/qs-netcat-s390x 29 | GOOS=linux GOARCH=s390x ${BUILD} -trimpath -buildvcs=false -ldflags="-extldflags=-static -s -w" -o ${OUT_DIR}/linux/qs-netcat-riscv64 30 | freebsd: 31 | GOOS=freebsd GOARCH=amd64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/freebsd/qs-netcat-amd64 32 | GOOS=freebsd GOARCH=386 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/freebsd/qs-netcat-386 33 | GOOS=freebsd GOARCH=arm ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/freebsd/qs-netcat-arm 34 | GOOS=freebsd GOARCH=arm64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/freebsd/qs-netcat-arm64 35 | openbsd: 36 | GOOS=openbsd GOARCH=amd64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/openbsd/qs-netcat-amd64 37 | GOOS=openbsd GOARCH=386 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/openbsd/qs-netcat-386 38 | GOOS=openbsd GOARCH=arm ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/openbsd/qs-netcat-arm 39 | GOOS=openbsd GOARCH=arm64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/openbsd/qs-netcat-arm64 40 | # GOOS=openbsd GOARCH=mips64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/openbsd/qs-netcat-mips64 41 | netbsd: 42 | GOOS=netbsd GOARCH=amd64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/netbsd/qs-netcat-amd64 43 | GOOS=netbsd GOARCH=386 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/netbsd/qs-netcat-386 44 | GOOS=netbsd GOARCH=arm ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/netbsd/qs-netcat-arm 45 | GOOS=netbsd GOARCH=arm64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/netbsd/qs-netcat-arm64 46 | android: # android builds require native development kit 47 | CC="${ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android${ANDROID_API}-clang" GOOS=android GOARCH=amd64 ${ANDROID_BUILD} -o ${OUT_DIR}/android/qs-netcat-amd64 48 | CC="${ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android${ANDROID_API}-clang" GOOS=android GOARCH=386 ${ANDROID_BUILD} -o ${OUT_DIR}/android/qs-netcat-386 49 | CC="${ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi${ANDROID_API}-clang" GOOS=android GOARCH=arm ${ANDROID_BUILD} -o ${OUT_DIR}/android/qs-netcat-arm 50 | CC="${ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android${ANDROID_API}-clang" GOOS=android GOARCH=arm64 ${ANDROID_BUILD} -o ${OUT_DIR}/android/qs-netcat-arm64 51 | android-apk: 52 | gomobile build -target android -androidapi ${ANDROID_API} -o ${OUT_DIR}/android/qs-netcat.apk 53 | ios: 54 | GOOS=ios GOARCH=amd64 CGO_ENABLED=1 CC=${CURRET_DIR}/clangwrap.sh ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/ios/qs-netcat-amd64 55 | GOOS=ios GOARCH=arm64 CGO_ENABLED=1 CC=${CURRET_DIR}/clangwrap.sh ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/ios/qs-netcat-arm64 56 | darwin: 57 | GOOS=darwin GOARCH=amd64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/darwin/qs-netcat-amd64 58 | GOOS=darwin GOARCH=arm64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/darwin/qs-netcat-arm64 59 | solaris: 60 | GOOS=solaris GOARCH=amd64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/solaris/qs-netcat-amd64 61 | illumos: 62 | GOOS=illumos GOARCH=amd64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/illumos/qs-netcat-amd64 63 | aix: 64 | GOOS=aix GOARCH=ppc64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/aix/qs-netcat-ppc64 65 | dragonfly: 66 | GOOS=dragonfly GOARCH=amd64 ${BUILD} ${BUILD_FLAGS} -o ${OUT_DIR}/dragonfly/qs-netcat-amd64 67 | clean: 68 | rm -rfv ./build 69 | 70 | all: linux windows darwin android freebsd openbsd netbsd dragonfly # solaris illumos aix # ios 71 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | const ( 12 | LOGGER_PROMPT = "[%s] %s " 13 | LOG_LEVEL_TRACE = iota 14 | LOG_LEVEL_DEBUG 15 | LOG_LEVEL_INFO 16 | LOG_LEVEL_WARN 17 | LOG_LEVEL_ERROR 18 | LOG_LEVEL_FATAL 19 | ) 20 | 21 | var ( 22 | red = color.New(color.FgRed).Add(color.Bold) 23 | yellow = color.New(color.FgYellow) 24 | blue = color.New(color.FgHiBlue) 25 | criticalRed = color.New(color.BgRed).Add(color.Bold) 26 | faintWhite = color.New(color.FgWhite).Add(color.Faint) 27 | logLevel = LOG_LEVEL_INFO 28 | ) 29 | 30 | // SetLevel sets the standard logger level. 31 | func SetLevel(level int) { 32 | if level > LOG_LEVEL_FATAL || 33 | level < LOG_LEVEL_TRACE { 34 | return 35 | } 36 | logLevel = level 37 | } 38 | 39 | // GetLevel returns the standard logger level. 40 | func GetLevel() int { 41 | return logLevel 42 | } 43 | 44 | // Print logs a message at level Info on the standard logger. 45 | func Print(args ...interface{}) { 46 | fmt.Print( 47 | fmt.Sprintf( 48 | "[%s] ", 49 | faintWhite.Sprint(time.Now().Format("03:04:05.000")), 50 | ), 51 | fmt.Sprint(args...), 52 | "\n", 53 | ) 54 | } 55 | 56 | // Trace logs a message at level Trace on the standard logger. 57 | func Trace(args ...interface{}) { 58 | if logLevel != LOG_LEVEL_TRACE { 59 | return 60 | } 61 | fmt.Print( 62 | fmt.Sprintf( 63 | LOGGER_PROMPT, 64 | faintWhite.Sprint(time.Now().Format("03:04:05.000")), 65 | faintWhite.Sprint("[TRACE]"), 66 | ), 67 | fmt.Sprint(args...), 68 | "\n", 69 | ) 70 | } 71 | 72 | // Debug logs a message at level Debug on the standard logger. 73 | func Debug(args ...interface{}) { 74 | if logLevel > LOG_LEVEL_DEBUG { 75 | return 76 | } 77 | fmt.Print( 78 | fmt.Sprintf( 79 | LOGGER_PROMPT, 80 | faintWhite.Sprint(time.Now().Format("03:04:05.000")), 81 | "[DEBUG]", 82 | ), 83 | fmt.Sprint(args...), 84 | "\n", 85 | ) 86 | } 87 | 88 | // Info logs a message at level Info on the standard logger. 89 | func Info(args ...interface{}) { 90 | if logLevel > LOG_LEVEL_INFO { 91 | return 92 | } 93 | fmt.Print( 94 | fmt.Sprintf( 95 | LOGGER_PROMPT, 96 | faintWhite.Sprint(time.Now().Format("03:04:05.000")), 97 | blue.Sprint("[INFO]"), 98 | ), 99 | fmt.Sprint(args...), 100 | "\n", 101 | ) 102 | } 103 | 104 | // Warn logs a message at level Warn on the standard logger. 105 | func Warn(args ...interface{}) { 106 | if logLevel > LOG_LEVEL_WARN { 107 | return 108 | } 109 | fmt.Print( 110 | fmt.Sprintf( 111 | LOGGER_PROMPT, 112 | faintWhite.Sprint(time.Now().Format("03:04:05.000")), 113 | yellow.Sprint("[WARN]"), 114 | ), 115 | fmt.Sprint(args...), 116 | "\n", 117 | ) 118 | } 119 | 120 | // Error logs a message at level Error on the standard logger. 121 | func Error(args ...interface{}) { 122 | if logLevel > LOG_LEVEL_ERROR { 123 | return 124 | } 125 | fmt.Print( 126 | fmt.Sprintf( 127 | LOGGER_PROMPT, 128 | faintWhite.Sprint(time.Now().Format("03:04:05.000")), 129 | red.Sprint("[ERROR]"), 130 | ), 131 | fmt.Sprint(args...), 132 | "\n", 133 | ) 134 | } 135 | 136 | // Fatal logs a message at level Fatal on the standard logger then the process will exit with status set to 1. 137 | func Fatal(args ...interface{}) { 138 | if logLevel > LOG_LEVEL_ERROR { 139 | return 140 | } 141 | fmt.Print( 142 | fmt.Sprintf( 143 | LOGGER_PROMPT, 144 | faintWhite.Sprint(time.Now().Format("03:04:05.000")), 145 | criticalRed.Sprint("[FATAL]"), 146 | ), 147 | red.Sprint(args...), 148 | "\n", 149 | ) 150 | os.Exit(1) 151 | } 152 | 153 | // Tracef logs a message at level Trace on the standard logger. 154 | func Tracef(format string, args ...interface{}) { 155 | if logLevel != LOG_LEVEL_TRACE { 156 | return 157 | } 158 | fmt.Print( 159 | fmt.Sprintf( 160 | LOGGER_PROMPT, 161 | faintWhite.Sprint(time.Now().Format("03:04:05.000")), 162 | faintWhite.Sprint("[TRACE]"), 163 | ), 164 | fmt.Sprintf(format, args...), 165 | "\n", 166 | ) 167 | } 168 | 169 | // Debugf logs a message at level Debug on the standard logger. 170 | func Debugf(format string, args ...interface{}) { 171 | if logLevel > LOG_LEVEL_DEBUG { 172 | return 173 | } 174 | fmt.Print( 175 | fmt.Sprintf( 176 | LOGGER_PROMPT, 177 | faintWhite.Sprint(time.Now().Format("03:04:05.000")), 178 | "[DEBUG]", 179 | ), 180 | fmt.Sprintf(format, args...), 181 | "\n", 182 | ) 183 | } 184 | 185 | // Printf logs a message at level Info on the standard logger. 186 | func Printf(format string, args ...interface{}) { 187 | fmt.Print( 188 | fmt.Sprintf( 189 | LOGGER_PROMPT, 190 | faintWhite.Sprint(time.Now().Format("03:04:05.000")), 191 | "", 192 | ), 193 | fmt.Sprintf(format, args...), 194 | "\n", 195 | ) 196 | } 197 | 198 | // Infof logs a message at level Info on the standard logger. 199 | func Infof(format string, args ...interface{}) { 200 | if logLevel > LOG_LEVEL_INFO { 201 | return 202 | } 203 | fmt.Print( 204 | fmt.Sprintf( 205 | LOGGER_PROMPT, 206 | faintWhite.Sprint(time.Now().Format("03:04:05.000")), 207 | blue.Sprint("[INFO]"), 208 | ), 209 | fmt.Sprintf(format, args...), 210 | "\n", 211 | ) 212 | } 213 | 214 | // Warnf logs a message at level Warn on the standard logger. 215 | func Warnf(format string, args ...interface{}) { 216 | if logLevel > LOG_LEVEL_WARN { 217 | return 218 | } 219 | fmt.Print( 220 | fmt.Sprintf( 221 | LOGGER_PROMPT, 222 | faintWhite.Sprint(time.Now().Format("03:04:05.000")), 223 | yellow.Sprint("[WARN]"), 224 | ), 225 | fmt.Sprintf(format, args...), 226 | "\n", 227 | ) 228 | } 229 | 230 | // Errorf logs a message at level Error on the standard logger. 231 | func Errorf(format string, args ...interface{}) { 232 | if logLevel > LOG_LEVEL_ERROR { 233 | return 234 | } 235 | fmt.Print( 236 | fmt.Sprintf( 237 | LOGGER_PROMPT, 238 | faintWhite.Sprint(time.Now().Format("03:04:05.000")), 239 | red.Sprint("[ERROR]"), 240 | ), 241 | fmt.Sprintf(format, args...), 242 | "\n", 243 | ) 244 | } 245 | 246 | // Fatalf logs a message at level Fatal on the standard logger then the process will exit with status set to 1. 247 | func Fatalf(format string, args ...interface{}) { 248 | if logLevel > LOG_LEVEL_ERROR { 249 | return 250 | } 251 | fmt.Print( 252 | fmt.Sprintf( 253 | LOGGER_PROMPT, 254 | faintWhite.Sprint(time.Now().Format("03:04:05.000")), 255 | criticalRed.Sprint("[FATAL]"), 256 | ), 257 | red.Sprintf(format, args...), 258 | "\n", 259 | ) 260 | os.Exit(1) 261 | } 262 | -------------------------------------------------------------------------------- /mkrelease.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ## ANSI Colors (FG & BG) 3 | RED="$(printf '\033[31m')" GREEN="$(printf '\033[32m')" YELLOW="$(printf '\033[33m')" BLUE="$(printf '\033[34m')" 4 | MAGENTA="$(printf '\033[35m')" CYAN="$(printf '\033[36m')" WHITE="$(printf '\033[37m')" BLACK="$(printf '\033[30m')" 5 | REDBG="$(printf '\033[41m')" GREENBG="$(printf '\033[42m')" YELLOWBG="$(printf '\033[43m')" BLUEBG="$(printf '\033[44m')" 6 | MAGENTABG="$(printf '\033[45m')" CYANBG="$(printf '\033[46m')" WHITEBG="$(printf '\033[47m')" BLACKBG="$(printf '\033[40m')" 7 | RESET="$(printf '\e[0m')" 8 | 9 | ## Globals 10 | RELEASE_DIR="`pwd`/release" 11 | BUILD_DIR="`pwd`/build" 12 | UPX_VERSION="4.1.0" 13 | ERR_LOG="/dev/null" 14 | [[ ! -z $VERBOSE ]] && ERR_LOG="`tty`" 15 | 16 | print_status() { 17 | echo ${YELLOW}"[*] ${RESET}${1}" 18 | } 19 | 20 | print_progress() { 21 | [[ ! -z "${VERBOSE}" ]] && return 22 | echo -n ${YELLOW}"[*] ${RESET}${1}" 23 | n=${#1} 24 | printf %$((70-$n))s |tr " " "." 25 | } 26 | 27 | print_warning() { 28 | echo -n ${YELLOW}"[!] ${RESET}${1}" 29 | } 30 | 31 | print_error() { 32 | echo ${RED}"[-] ${RESET}${1}" 33 | } 34 | 35 | print_fatal() { 36 | echo -e ${RED}"[!] $1\n${RESET}" 37 | kill -10 $$ 38 | } 39 | 40 | print_good() { 41 | echo ${GREEN}"[+] ${RESET}${1}" 42 | } 43 | 44 | print_verbose() { 45 | if [[ ! -z "${VERBOSE}" ]]; then 46 | echo ${WHITE}"[*] ${RESET}${1}" 47 | fi 48 | } 49 | 50 | print_ok(){ 51 | [[ -z "${VERBOSE}" ]] && echo -e " [${GREEN}OK${RESET}]" 52 | } 53 | 54 | print_fail(){ 55 | [[ -z "${VERBOSE}" ]] && echo -e " [${RED}FAIL${RESET}]" 56 | } 57 | 58 | must_exist() { 59 | for i in "$@"; do 60 | command -v $i >$ERR_LOG || print_fatal "$i not installed! Exiting..." 61 | done 62 | } 63 | 64 | one_must_exist() { 65 | command -v $1 >$ERR_LOG || command -v $2 >$ERR_LOG || print_fatal "Neither $1 nor $2 installed! Exiting..." 66 | } 67 | 68 | ## Handle SININT 69 | exit_on_signal_SIGINT () { 70 | echo "" 71 | print_error "Script interrupted!" 72 | clean_exit 73 | } 74 | 75 | exit_on_signal_SIGTERM () { 76 | echo "" 77 | print_error "Script interrupted!" 78 | clean_exit 79 | } 80 | 81 | trap exit_on_signal_SIGINT SIGINT 82 | trap exit_on_signal_SIGTERM SIGTERM 83 | 84 | 85 | # Remove all artifacts and exit... 86 | clean_exit() { 87 | [[ -d "$RELEASE_DIR" ]] && rm -rf "$RELEASE_DIR" &>$ERR_LOG 88 | kill -10 $$ 89 | } 90 | 91 | download_upx() { 92 | print_status "Downloading UPX v$UPX_VERSION ..." 93 | wget "https://github.com/upx/upx/releases/download/v$UPX_VERSION/upx-$UPX_VERSION-amd64_linux.tar.xz" -O "/tmp/upx.tar.xz" &>$ERR_LOG || return 1 94 | tar -C /tmp/ -xvf "/tmp/upx.tar.xz" &>$ERR_LOG || return 1 95 | cp "/tmp/upx-$UPX_VERSION-amd64_linux/upx" "$BUILD_DIR/upx" &>$ERR_LOG || return 1 96 | chmod +x "$BUILD_DIR/upx" &>$ERR_LOG || return 1 97 | return 0 98 | } 99 | 100 | # Expects and creates a tar.gz archive after compressing the binary with UPX. 101 | # $1 = 102 | # $2 = 103 | package_release_binary() { 104 | local bin_suffix="" 105 | [[ $1 == "windows" ]] && bin_suffix=".exe" 106 | cp "$BUILD_DIR/$1/qs-netcat-${2}${bin_suffix}" "$BUILD_DIR/qs-netcat${bin_suffix}" &>$ERR_LOG || return 1 107 | if [ "$3" != false ]; then 108 | print_verbose "Compressing $BUILD_DIR/$1/qs-netcat-$2${bin_suffix}..." 109 | "$BUILD_DIR/upx" -q --best "$BUILD_DIR/qs-netcat${bin_suffix}" &>$ERR_LOG # Ignore errors... 110 | fi 111 | print_verbose "Packaging $BUILD_DIR/$1/qs-netcat-$2${bin_suffix}..." 112 | tar -C "$BUILD_DIR" -czvf "$RELEASE_DIR/qs-netcat_$1_$arc.tar.gz" "./qs-netcat${bin_suffix}" &>$ERR_LOG || return 1 113 | return 0 114 | } 115 | 116 | [[ ! -d $BUILD_DIR ]] && print_fatal "Could not find build firectory! Exiting..." 117 | print_status "Initiating..." 118 | print_status "Release Date: `date`" 119 | download_upx || print_fatal "Failed downloading UPX!" 120 | echo "" 121 | mkdir -p release 122 | declare -a arcs=("amd64" "386" "arm" "arm64" "mips" "mips64" "mips64le" "mipsle" "ppc64" "ppc64le" "s390x" "riscv64") 123 | for arc in "${arcs[@]}" 124 | do 125 | print_progress "Packaging linux-$arc binary" 126 | package_release_binary "linux" $arc && print_ok || print_fail 127 | done 128 | 129 | declare -a arcs=("amd64" "386" "arm" "arm64") 130 | for arc in "${arcs[@]}" 131 | do 132 | print_progress "Packaging windows-$arc binary" 133 | package_release_binary "windows" $arc && print_ok || print_fail 134 | done 135 | 136 | declare -a arcs=("amd64" "arm64") 137 | for arc in "${arcs[@]}" 138 | do 139 | print_progress "Packaging darwin-$arc binary" 140 | package_release_binary "darwin" $arc "false" && print_ok || print_fail 141 | done 142 | 143 | # declare -a arcs=("amd64" "arm64") 144 | # for arc in "${arcs[@]}" 145 | # do 146 | # echo -n "[*] Packaging ios-$arc binary -> " 147 | # cp "$BUILD_DIR/ios/qs-netcat-$arc" "$BUILD_DIR/qs-netcat" 148 | # upx -q --best "$BUILD_DIR/qs-netcat" &>/dev/null 149 | # tar -C "$BUILD_DIR" -czvf "$RELEASE_DIR/qs-netcat_ios_$arc.tar.gz" "./qs-netcat" 150 | # done 151 | 152 | declare -a arcs=("amd64" "386" "arm" "arm64") 153 | for arc in "${arcs[@]}" 154 | do 155 | print_progress "Packaging android-$arc binary" 156 | package_release_binary "android" $arc && print_ok || print_fail 157 | done 158 | 159 | declare -a arcs=("amd64" "386" "arm" "arm64") 160 | for arc in "${arcs[@]}" 161 | do 162 | print_progress "Packaging freebsd-$arc binary" 163 | package_release_binary "freebsd" $arc && print_ok || print_fail 164 | done 165 | 166 | declare -a arcs=("amd64" "arm" "arm64" "mips64") 167 | for arc in "${arcs[@]}" 168 | do 169 | print_progress "Packaging openbsd-$arc binary" 170 | package_release_binary "openbsd" $arc && print_ok || print_fail 171 | done 172 | 173 | declare -a arcs=("amd64" "386" "arm" "arm64") 174 | for arc in "${arcs[@]}" 175 | do 176 | print_progress "Packaging netbsd-$arc binary" 177 | package_release_binary "netbsd" $arc && print_ok || print_fail 178 | done 179 | 180 | # Special distro cases... 181 | print_progress "Packaging android APK" 182 | cp "$BUILD_DIR/android/qs-netcat.apk" "$RELEASE_DIR/" &>$ERR_LOG && print_ok || print_fail 183 | 184 | print_progress "Packaging solaris-amd64 binary" 185 | package_release_binary "netbsd" "amd64" && print_ok || print_fail 186 | 187 | print_progress "Packaging illumos-amd64 binary" 188 | package_release_binary "illumos" "amd64" && print_ok || print_fail 189 | 190 | print_progress "Packaging dragonfly-amd64 binary" 191 | package_release_binary "dragonfly" "amd64" && print_ok || print_fail 192 | 193 | print_progress "Packaging aix-ppc64 binary" 194 | package_release_binary "aix" "ppc64" && print_ok || print_fail 195 | 196 | print_good "All done!" 197 | 198 | cd $RELEASE_DIR 199 | echo -e "\n\`\`\`" 200 | sha1sum * 201 | echo -e "\`\`\`\n" 202 | -------------------------------------------------------------------------------- /pkg/netcat.go: -------------------------------------------------------------------------------- 1 | package qsnetcat 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net" 8 | "os" 9 | "time" 10 | 11 | "github.com/qsocket/qs-netcat/config" 12 | "github.com/qsocket/qs-netcat/log" 13 | qsocket "github.com/qsocket/qsocket-go" 14 | 15 | "github.com/briandowns/spinner" 16 | "golang.org/x/term" 17 | ) 18 | 19 | var ( 20 | ErrQsocketSessionEnd = errors.New("QSocket session has ended") 21 | ErrTtyFailed = errors.New("TTY initialization failed") 22 | ErrUntrustedCert = errors.New("Certificate fingerprint mismatch") 23 | spn = spinner.New(spinner.CharSets[9], 50*time.Millisecond) 24 | ) 25 | 26 | func ProbeQSRN(opts *config.Options) error { 27 | // This is nessesary for persistence on windows 28 | os.Unsetenv("QS_ARGS") // Remove this for allowing recursive qs-netcat usage 29 | qs := qsocket.NewSocket(GetPeerTag(opts), opts.Secret) 30 | err := qs.SetE2E(opts.End2End) 31 | if err != nil { 32 | return err 33 | } 34 | if opts.CertFingerprint != "" { 35 | err = qs.SetCertFingerprint(opts.CertFingerprint) 36 | if err != nil { 37 | return err 38 | } 39 | } 40 | if opts.SocksAddr != "" { 41 | err = qs.SetProxy(opts.SocksAddr) 42 | if err != nil { 43 | return err 44 | } 45 | } 46 | 47 | // Dial QSRN... 48 | err = qs.Dial(!opts.DisableEnc) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | log.Debug("Recving session specs...") 54 | specs, err := RecvSessionSpecs(qs, opts) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | log.Info("Starting new session...") 60 | // Resize terminal with client dimentions... 61 | log.Debugf("Got term size: %dx%d", specs.TermSize.Rows, specs.TermSize.Cols) 62 | log.Debugf("Got command: %s", specs.Command) 63 | log.Debugf("Got forward: %s", specs.ForwardAddr) 64 | 65 | // First check if forwarding enabled 66 | if specs.ForwardAddr != "" { 67 | // Redirect traffic to forward addr 68 | err = CreatePipeOnConnect(qs, specs.ForwardAddr) 69 | if err != nil { 70 | return err 71 | } 72 | } 73 | 74 | if opts.IsPiped() { 75 | return AttachToPipe(qs, opts) 76 | } 77 | 78 | go func() { 79 | // Execute command/program and redirect stdin/out/err 80 | err = ExecCommand(qs, *specs) 81 | if err != nil { 82 | log.Error(err) 83 | } 84 | }() 85 | return err 86 | } 87 | 88 | func CreatePipeOnConnect(qs *qsocket.QSocket, addr string) error { 89 | defer qs.Close() 90 | conn, err := net.Dial("tcp", addr) 91 | if err != nil { 92 | return err 93 | } 94 | defer conn.Close() 95 | 96 | ch := make(chan bool, 1) 97 | go func() { 98 | _, err = io.Copy(conn, qs) 99 | ch <- true 100 | }() 101 | go func() { 102 | _, err = io.Copy(qs, conn) 103 | ch <- true 104 | }() 105 | <-ch 106 | return err 107 | } 108 | 109 | func InitLocalProxy(qs *qsocket.QSocket, opts *config.Options) { 110 | ln, err := net.Listen("tcp", ":"+opts.LocalPort) 111 | if err != nil { 112 | log.Fatal(err) 113 | } 114 | 115 | for { 116 | spn.Suffix = " Waiting for local connection..." 117 | spn.Start() 118 | conn, err := ln.Accept() 119 | if err != nil { 120 | log.Error(err) 121 | continue 122 | } 123 | spn.Suffix = " Dialing qsocket relay network..." 124 | if opts.SocksAddr != "" { 125 | err = qs.SetProxy(opts.SocksAddr) 126 | if err != nil { 127 | log.Fatal(err) 128 | } 129 | } 130 | 131 | err = qs.Dial(!opts.DisableEnc) 132 | if err != nil { 133 | spn.Stop() 134 | log.Error(err) 135 | continue 136 | } 137 | 138 | err = SendSessionSpecs(qs, opts) 139 | if err != nil { 140 | spn.Stop() 141 | log.Error(err) 142 | continue 143 | } 144 | 145 | spn.Suffix = " Forwarding local traffic..." 146 | go func() { 147 | _, err = io.Copy(conn, qs) 148 | }() 149 | _, err = io.Copy(qs, conn) 150 | if err != nil { 151 | log.Debug(err) 152 | } 153 | qs.Close() 154 | conn.Close() 155 | } 156 | } 157 | 158 | func Connect(opts *config.Options) error { 159 | defer spn.Stop() 160 | if opts == nil { 161 | return errors.New("Options are not initialized") 162 | } 163 | if !opts.Quiet { 164 | spn.Suffix = " Dialing qsocket relay network..." 165 | spn.Start() 166 | } 167 | 168 | qs := qsocket.NewSocket(GetPeerTag(opts), opts.Secret) 169 | err := qs.SetE2E(opts.End2End) 170 | if err != nil { 171 | log.Fatal(err) 172 | } 173 | if opts.CertFingerprint != "" { 174 | err = qs.SetCertFingerprint(opts.CertFingerprint) 175 | if err != nil { 176 | log.Fatal(err) 177 | } 178 | } 179 | 180 | if opts.LocalPort != "" { 181 | InitLocalProxy(qs, opts) 182 | } 183 | 184 | if opts.SocksAddr != "" { 185 | err = qs.SetProxy(opts.SocksAddr) 186 | if err != nil { 187 | log.Fatal(err) 188 | } 189 | } 190 | 191 | err = qs.Dial(!opts.DisableEnc) 192 | if err != nil { 193 | return err 194 | } 195 | 196 | log.Debug("Sending session specs...") 197 | err = SendSessionSpecs(qs, opts) 198 | if err != nil { 199 | return err 200 | } 201 | 202 | if opts.IsPiped() { 203 | return AttachToPipe(qs, opts) 204 | } 205 | return AttachToSocket(qs, opts.Interactive) 206 | } 207 | 208 | func AttachToPipe(conn *qsocket.QSocket, opts *config.Options) error { 209 | finalMsg := "No pipe is initialized..." 210 | defer func() { log.Info(finalMsg) }() 211 | defer conn.Close() 212 | if opts.InPipe != nil { 213 | if !opts.Listen { 214 | spn.Suffix = fmt.Sprintf(" Reading from %s...", opts.InPipe.Name()) 215 | spn.Start() 216 | defer spn.Stop() 217 | } 218 | total := 0 219 | for { 220 | data := make([]byte, 1024) 221 | n, err := opts.InPipe.Read(data) 222 | if err != nil { 223 | return err 224 | } 225 | if n == 0 { 226 | continue 227 | } 228 | total += n 229 | finalMsg = fmt.Sprintf("Sent %d bytes!", total) 230 | spn.Suffix = fmt.Sprintf(" Piping %d bytes from %s...", total, opts.InPipe.Name()) 231 | n, err = conn.Write(data[:n]) 232 | if err != nil { 233 | return err 234 | } 235 | } 236 | } else if opts.OutPipe != nil { 237 | if !opts.Listen { 238 | spn = spinner.New(spinner.CharSets[9], 50*time.Millisecond, spinner.WithWriter(os.Stderr)) 239 | spn.Suffix = fmt.Sprintf(" Writing into %s...", opts.OutPipe.Name()) 240 | spn.Start() 241 | defer spn.Stop() 242 | } 243 | total := 0 244 | for { 245 | data := make([]byte, 1024) 246 | n, err := conn.Read(data) 247 | if err != nil { 248 | return err 249 | } 250 | if n == 0 { 251 | continue 252 | } 253 | total += n 254 | finalMsg = fmt.Sprintf("Received %d bytes!", total) 255 | spn.Suffix = fmt.Sprintf(" Piping %d bytes to %s...", total, opts.OutPipe.Name()) 256 | n, err = opts.OutPipe.Write(data[:n]) 257 | if err != nil { 258 | return err 259 | } 260 | } 261 | } 262 | 263 | return nil 264 | } 265 | 266 | func AttachToSocket(conn *qsocket.QSocket, interactive bool) error { 267 | defer conn.Close() 268 | if interactive { 269 | spn.Suffix = " Setting up TTY terminal..." 270 | oldState, err := term.MakeRaw(int(os.Stdin.Fd())) 271 | if err != nil { 272 | return err 273 | } 274 | defer term.Restore(int(os.Stdin.Fd()), oldState) 275 | } 276 | spn.Stop() 277 | go func() { io.Copy(conn, os.Stdin) }() 278 | io.Copy(os.Stdout, conn) 279 | return nil 280 | } 281 | 282 | func GetPeerTag(opts *config.Options) qsocket.SocketType { 283 | if opts.Listen { 284 | return qsocket.Server 285 | } 286 | return qsocket.Client 287 | } 288 | -------------------------------------------------------------------------------- /config/options.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "regexp" 9 | "strings" 10 | "time" 11 | 12 | "github.com/alecthomas/kong" 13 | "github.com/fatih/color" 14 | "github.com/mdp/qrterminal/v3" 15 | "github.com/qsocket/qs-netcat/log" 16 | "github.com/qsocket/qs-netcat/utils" 17 | ) 18 | 19 | const ( 20 | USAGE_EAMPLES = ` 21 | Example to forward traffic from port 2222 to 192.168.6.7:22: 22 | $ qs-netcat -s MyCecret -f 2222:192.168.6.7:22 23 | Example file transfer: 24 | $ qs-netcat -l -s MyCecret > warez.tar.gz # Server 25 | $ qs-netcat -s MyCecret < warez.tar.gz # Client 26 | Example for a reverse shell: 27 | $ qs-netcat -s MyCecret -l -i # Server 28 | $ qs-netcat -s MyCecret -i # Client 29 | ` 30 | DEFAULT_E2E_CIPHER = "SRP-AES-GCM-256-E2E (Prime: 4096)" 31 | ) 32 | 33 | var ( 34 | ForwardAddrRgx = regexp.MustCompile(`(?P([0-9]{1,5}):|)(?P(?:[0-9]{1,3}\.){3}[0-9]{1,3}):(?P[0-9]{1,5})`) 35 | Version = "?" 36 | ) 37 | 38 | // Main config struct for parsing the TOML file 39 | type Options struct { 40 | Secret string `help:"Secret (e.g. password)." name:"secret" short:"s"` 41 | Execute string `help:"Execute command [e.g. \"bash -il\" or \"cmd.exe\"]" name:"exec" short:"e"` 42 | ForwardAddr string `help:"IP:PORT or PORT:IP:PORT for port forwarding." name:"forward" short:"f"` 43 | SocksAddr string `help:"User socks proxy address for connecting QSRN." name:"socks" short:"x"` 44 | CertFingerprint string `help:"Hex encoded TLS certificate fingerprint for validation." name:"cert-fp"` 45 | ProbeInterval int `help:"Probe interval for connecting QSRN." name:"probe" short:"n" default:"5"` 46 | DisableEnc bool `help:"Disable all encryption." name:"plain" short:"C"` 47 | End2End bool `help:"Use E2E encryption. (default:true)" name:"e2e" default:"true"` 48 | Interactive bool `help:"Execute with a PTY shell." name:"interactive" short:"i"` 49 | Listen bool `help:"Server mode. (listen for connections)" name:"listen" short:"l"` 50 | RandomSecret bool `help:"Generate a Secret. (random)" name:"generate" short:"g"` 51 | CertPinning bool `help:"Enable certificate pinning on TLS connections." name:"pin" short:"K"` 52 | Quiet bool `help:"Quiet mode. (no stdout)" name:"quiet" short:"q"` 53 | UseTor bool `help:"Use TOR for connecting QSRN." name:"tor" short:"T"` 54 | GenerateQR bool `help:"Generate a QR code with given stdin and print on the terminal." name:"qr"` 55 | Verbose bool `help:"Verbose mode." name:"verbose" short:"v"` 56 | LocalPort string `kong:"-"` 57 | InPipe *os.File `kong:"-"` 58 | OutPipe *os.File `kong:"-"` 59 | Version kong.VersionFlag 60 | } 61 | 62 | func HelpPrompt(options kong.HelpOptions, ctx *kong.Context) error { 63 | err := kong.DefaultHelpPrinter(options, ctx) 64 | if err != nil { 65 | return err 66 | } 67 | print(USAGE_EAMPLES) 68 | return nil 69 | } 70 | 71 | // ConfigureOptions accepts a flag set and augments it with agentgo-server 72 | // specific flags. On success, an options structure is returned configured 73 | // based on the selected flags. 74 | func ConfigureOptions() (*Options, error) { 75 | if os.Args == nil { 76 | return nil, errors.New("os.Args is nil") 77 | } 78 | // If QS_ARGS exists overwrite the given arguments. 79 | qsArgs := os.Getenv("QS_ARGS") 80 | args := os.Args[1:] 81 | if qsArgs != "" { 82 | args = strings.Split(qsArgs, " ") 83 | } 84 | 85 | // Parse arguments and check for errors 86 | opts := &Options{} 87 | parser, err := kong.New( 88 | opts, 89 | kong.Help(HelpPrompt), 90 | kong.UsageOnError(), 91 | kong.Vars{"version": Version}, 92 | kong.ConfigureHelp(kong.HelpOptions{ 93 | Summary: true, 94 | }), 95 | ) 96 | if err != nil { 97 | return nil, err 98 | } 99 | _, err = parser.Parse(args) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | // Disable smart pipe if parameters are being passed via env 105 | if utils.IsFilePiped(os.Stdin) && qsArgs == "" { 106 | opts.InPipe = os.Stdin 107 | } 108 | 109 | if utils.IsFilePiped(os.Stdout) && qsArgs == "" { 110 | opts.OutPipe = os.Stdout 111 | utils.EnableSmartPipe() 112 | } 113 | 114 | if opts.GenerateQR { 115 | in, err := io.ReadAll(os.Stdin) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | qcfg := qrterminal.Config{ 121 | Level: qrterminal.M, 122 | Writer: os.Stdout, 123 | BlackChar: qrterminal.WHITE, 124 | WhiteChar: qrterminal.BLACK, 125 | QuietZone: 1, 126 | } 127 | 128 | qrterminal.GenerateWithConfig(string(in), qcfg) 129 | os.Exit(0) 130 | } 131 | 132 | // Generate random secret 133 | if !opts.Listen && opts.RandomSecret { 134 | print(utils.RandomString(20)) 135 | os.Exit(0) 136 | } 137 | 138 | if opts.ForwardAddr != "" && !ForwardAddrRgx.MatchString(opts.ForwardAddr) { 139 | subMatches := ForwardAddrRgx.FindStringSubmatch(opts.ForwardAddr) 140 | lport := ForwardAddrRgx.SubexpIndex("lport") 141 | rhost := ForwardAddrRgx.SubexpIndex("rhost") 142 | rport := ForwardAddrRgx.SubexpIndex("rport") 143 | opts.LocalPort = subMatches[lport] 144 | opts.ForwardAddr = fmt.Sprintf("%s:%s", subMatches[rhost], subMatches[rport]) 145 | return nil, errors.New("Invalid forward address.") 146 | } 147 | 148 | if opts.UseTor { 149 | opts.SocksAddr = "127.0.0.1:9050" 150 | } 151 | 152 | if opts.DisableEnc { 153 | opts.End2End = false 154 | } 155 | 156 | if !opts.RandomSecret && opts.Secret == "" { 157 | color.New(color.FgBlue).Add(color.Bold).Print("[>] ") 158 | print("Enter Secret (or press Enter to generate): ") 159 | n, _ := fmt.Scanln(&opts.Secret) 160 | if n == 0 { 161 | opts.RandomSecret = true 162 | } 163 | } 164 | 165 | if opts.Verbose { 166 | log.SetLevel(log.LOG_LEVEL_TRACE) // Show all the shit! 167 | } 168 | 169 | if opts.Quiet { 170 | log.SetLevel(log.LOG_LEVEL_FATAL) // Show nothing! 171 | } 172 | 173 | return opts, nil 174 | } 175 | 176 | func (opts *Options) IsPiped() bool { 177 | return opts.InPipe != nil || opts.OutPipe != nil 178 | } 179 | 180 | func (opts *Options) Summarize() { 181 | if opts == nil || opts.Quiet { 182 | return 183 | } 184 | encType := DEFAULT_E2E_CIPHER 185 | mode := "client" 186 | if opts.Listen { 187 | mode = "server" 188 | } 189 | 190 | fmt.Printf("%s %s\n", utils.BoldYellow.Sprintf("[#]"), utils.BoldBlue.Sprintf(".::Qsocket Netcat::.")) 191 | fmt.Printf("%s Secret: %s\n", utils.Yellow.Sprintf(" ├──>"), utils.Red.Sprintf(opts.Secret)) 192 | fmt.Printf("%s Mode: %s\n", utils.Yellow.Sprintf(" ├──>"), mode) 193 | fmt.Printf("%s TOR: %t\n", utils.Yellow.Sprintf(" ├──>"), opts.UseTor) 194 | if opts.ForwardAddr != "" { 195 | fmt.Printf("%s Forward: %s\n", utils.Yellow.Sprintf(" ├──>"), opts.ForwardAddr) 196 | } 197 | if opts.Listen { 198 | fmt.Printf("%s Probe Interval: %s\n", utils.Yellow.Sprintf(" ├──>"), time.Second*time.Duration(opts.ProbeInterval)) 199 | } else { 200 | fmt.Printf("%s Probe Duration: %s\n", utils.Yellow.Sprintf(" ├──>"), time.Second*time.Duration(opts.ProbeInterval)) 201 | } 202 | if opts.InPipe != nil || opts.OutPipe != nil { 203 | fmt.Printf("%s Pipe: true\n", utils.Yellow.Sprintf(" ├──>")) 204 | } 205 | if opts.DisableEnc { 206 | encType = utils.Red.Sprintf("DISABLED") 207 | } else { 208 | if !opts.End2End { 209 | encType = "TLS" 210 | } 211 | } 212 | fmt.Printf("%s Encryption: %s\n", utils.Yellow.Sprintf(" └──>"), encType) 213 | print("\n") 214 | } 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 | 7 | [![GitHub All Releases][release-img]][release] 8 | [![Build][workflow-img]][workflow] 9 | [![Issues][issues-img]][issues] 10 | [![Go Report Card][go-report-img]][go-report] 11 | ![Docker Pulls][docker-pulls] 12 | [![License: MIT][license-img]][license] 13 |
14 | 15 | [go-report]: https://goreportcard.com/report/github.com/qsocket/qs-netcat 16 | [go-report-img]: https://goreportcard.com/badge/github.com/qsocket/qs-netcat 17 | [release]: https://github.com/qsocket/qs-netcat/releases 18 | [release-img]: https://img.shields.io/github/v/release/qsocket/qs-netcat 19 | [downloads]: https://github.com/qsocket/qs-netcat/releases 20 | [downloads-img]: https://img.shields.io/github/downloads/qsocket/qs-netcat/total?logo=github 21 | [issues]: https://github.com/qsocket/qs-netcat/issues 22 | [issues-img]: https://img.shields.io/github/issues/qsocket/qs-netcat?color=red 23 | [docker-pulls]: https://img.shields.io/docker/pulls/qsocket/qsocket?logo=docker&label=docker%20pulls 24 | [license]: https://raw.githubusercontent.com/qsocket/qs-netcat/master/LICENSE 25 | [license-img]: https://img.shields.io/github/license/qsocket/qs-netcat.svg 26 | [google-cloud-shell]: https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/qsocket/qs-netcat&tutorial=README.md 27 | [workflow-img]: https://github.com/qsocket/qs-netcat/actions/workflows/main.yml/badge.svg 28 | [workflow]: https://github.com/qsocket/qs-netcat/actions/workflows/main.yml 29 | [qsrn]: https://www.qsocket.io/qsrn/ 30 | 31 | qs-netcat is a cross-platform networking utility which reads and writes E2E encrypted data across systems using the QSocket relay network ([QSRN][qsrn]). 32 | It allows redirecting fully interactive PTY sessions with reverse connections, effectively allowing remote access to systems, forwarding traffic, and transferring files to and from systems under NAT networks or firewalls. 33 | 34 | > [!WARNING] 35 | > This tool is in its early alpha development stage, featuring experimental functionality that may lack backwards compatibility, and users are advised to exercise caution and not use it in production environments. 36 | 37 | > [!CAUTION] 38 | > Due to the changes to the relay protocol, clients starting from version `v0.0.8-beta` will not be compatible with the older versions. You can still access your older instances using previous client versions `(>= v.0.0.7-beta)` until October 23, 2024. **After October 23, 2024, legacy relay support will end, and all out-of-date QSocket instances will become inaccessible!** 39 | 40 | 41 | 42 | ## Installation 43 | 44 | [![Open in Cloud Shell](.github/img/cloud-shell.png)][google-cloud-shell] 45 | 46 | | **Tool** | **Build From Source** | **Docker Image** | **Binary Release** | 47 | |:-------------:|:----------------------------------------------------:|:---------------------------:|:---------------------------------------------------------:| 48 | | **qs-netcat** | ```go install github.com/qsocket/qs-netcat@master``` | [Download](#docker-install) | [Download](release) | 49 | 50 | --- 51 | 52 | qs-netcat supports 10 architectures and 12 operating systems, following table contains detailed list of all **Supported Platforms**. 53 | 54 |
55 | Supported Platforms 56 | 57 | - ✅ `Supported` 58 | - 🚧 `In progress` 59 | - ❌ `Unsupported` 60 | 61 | | **ARCH/OS** | **Linux** | **MacOS** | **Windows** | **Android** | **IOS** | **FreeBSD** | **OpenBSD** | **NetBSD** | **Solaris** | **Illumos** | **Dragonfly** | **AIX** | 62 | |:-----------:|:---------:|:---------:|:-----------:|:-----------:|:-------:|:-----------:|:-----------:|:----------:|:-----------:|:-----------:|:-------------:|:-------:| 63 | | **AMD64** | ✅ | ✅ | ✅ | ✅ | 🚧 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | 64 | | **ARM64** | ✅ | ✅ | ✅ | ✅ | 🚧 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | 65 | | **386** | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | 66 | | **ARM32** | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | 67 | | **RISCV64** | 🚧 | ❌ | ❌ | ❌ | ❌ | 🚧 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 68 | | **MIPS64** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 69 | | **MIPS32** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 70 | | **MIPSLE** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 71 | | **PPC64** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | 🚧 | ❌ | ❌ | ❌ | ❌ | 🚧 | 72 | | **PPC64LE** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 73 | | **S390X** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 74 | 75 |
76 | 77 | 78 | ### Docker Install 79 | 80 | [![Docker](http://dockeri.co/image/qsocket/qsocket)](https://hub.docker.com/r/qsocket/qsocket/) 81 | 82 | ```bash 83 | docker pull qsocket/qsocket:latest 84 | docker run -it qsocket -h 85 | ``` 86 | 87 | ## Usage 88 | 89 | ``` 90 | Usage: qs-netcat 91 | 92 | Flags: 93 | -h, --help Show context-sensitive help. 94 | -s, --secret=STRING Secret (e.g. password). 95 | -e, --exec=STRING Execute command [e.g. "bash -il" or "cmd.exe"] 96 | -f, --forward=STRING IP:PORT for traffic forwarding. 97 | -x, --socks=STRING User socks proxy address for connecting QSRN. 98 | --cert-fp=STRING Hex encoded TLS certificate fingerprint for validation. 99 | -n, --probe=5 Probe interval for connecting QSRN. 100 | -C, --plain Disable all encryption. 101 | --e2e Use E2E encryption. (default:true) 102 | -i, --interactive Execute with a PTY shell. 103 | -l, --listen Server mode. (listen for connections) 104 | -g, --generate Generate a Secret. (random) 105 | -K, --pin Enable certificate pinning on TLS connections. 106 | -q, --quiet Quiet mode. (no stdout) 107 | -T, --tor Use TOR for connecting QSRN. 108 | --qr Generate a QR code with given stdin and print on the terminal. 109 | -v, --verbose Verbose mode. 110 | --in-pipe=IN-PIPE 111 | --out-pipe=OUT-PIPE 112 | --version 113 | 114 | Example to forward traffic from port 2222 to 192.168.6.7:22: 115 | $ qs-netcat -s MyCecret -f 2222:192.168.6.7:22 116 | Example file transfer: 117 | $ qs-netcat -l -s MyCecret > warez.tar.gz # Server 118 | $ qs-netcat -s MyCecret < warez.tar.gz # Client 119 | Example for a reverse shell: 120 | $ qs-netcat -s MyCecret -l -i # Server 121 | $ qs-netcat -s MyCecret -i # Client 122 | ``` 123 | 124 | ### Examples 125 | - Log in to Workstation A from Workstation B through any firewall/NAT 126 | ```bash 127 | qs-netcat -l -i # Workstation A 128 | qs-netcat -i # Workstation B 129 | ``` 130 | 131 | - SSH from *Workstation A* to *Workstation B* by port forwarding through any firewall/NAT 132 | ```bash 133 | qs-netcat -l # Workstation B 134 | qs-netcat -f "22:localhost:22" # Workstation A 135 | ssh user@localhost # Workstation A 136 | ``` 137 | 138 | - Transfer files from *Workstation B* to *Workstation A* using smart pipes 139 | ```bash 140 | qs-netcat -s MySecret -l > file.txt # Workstation A 141 | qs-netcat -s MySecret < file.txt # Workstation B 142 | ``` 143 | 144 | - Port forward. Access 192.168.6.7:80 on Workstation A's private LAN from Workstation B: 145 | ```bash 146 | qs-netcat -l # Workstation A 147 | qs-netcat -f 192.168.6.7:80 # Workstation B 148 | ``` 149 | 150 | - Execute any command (nc -e style) on *Workstation A* 151 | ```bash 152 | qs-netcat -l # Workstation A 153 | qs-netcat -e "echo hello_world; id" # Workstation B 154 | ``` 155 | - Access entirety of Workstation A's private LAN (Sock4/4a/5 proxy) 156 | ```bash 157 | qs-netcat -l # Workstation A 158 | qs-netcat -f "22:localhost:22" # Workstation B 159 | ssh -D 9090 root@localhost # Workstation B 160 | # Access www.google.com via Workstation A's private LAN from your Workstation B: 161 | curl --socks4a 127.1:9090 http://www.google.com 162 | ``` 163 | 164 | - Mount a remote folder of Workstation A using sshfs and qs-netcat 165 | ```bash 166 | qs-netcat -l # Workstation A 167 | qs-netcat -f "22:localhost:22" # Workstation B 168 | sudo sshfs -o allow_other,default_permissions root@localhost:/remote_dir /mnt/local_dir # Workstation B 169 | ``` 170 | 171 | --- 172 | 173 | **Crypto / Security Mumble Jumble** 174 | - The connections are end-2-end encrypted. This means from User-2-User (and not just to the Relay Network). The Relay Network relays only (encrypted) data to and from the Users. 175 | - The QSocket uses [SRP](https://en.wikipedia.org/wiki/Secure_Remote_Password_protocol) for ensuring [perfect forward secrecy](https://en.wikipedia.org/wiki/Forward_secrecy). This means that the session keys are always different, and recorded session traffic cannot be decrypted by the third parties even if the user secret is known. 176 | - The session key is 256 bit and ephemeral. It is freshly generated for every session and generated randomly (and is not based on the password). 177 | - A brute force attack against weak secrets requires a new TCP connection for every guess. But QSRN contains a strong load balancer which is limiting the consecutive connection attempts. 178 | - Do not use stupid passwords like 'password123'. Malice might pick the same (stupid) password by chance and connect. If in doubt use *qs-netcat -g* to generate a strong one. Alice's and Bob's password should at least be strong enough so that Malice can not guess it by chance while Alice is waiting for Bob to connect. 179 | - If Alice shares the same password with Bob and Charlie and either one of them connects then Alice can not tell if it is Bob or Charlie who connected. 180 | - Assume Alice shares the same password with Bob and Malice. When Alice stops listening for a connection then Malice could start to listen for the connection instead. Bob (when opening a new connection) can not tell if he is connecting to Alice or to Malice. 181 | - We did not invent SRP. It's a well-known protocol, and it is well-analyzed and trusted by the community. 182 | 183 | 184 | https://user-images.githubusercontent.com/17179401/224060762-e0f121f6-431b-4eb5-8833-4a5d533003de.mp4 185 | 186 | --- 187 | 188 |
189 | RDP connection over QSRN 190 | 191 | https://github.com/qsocket/qs-netcat/assets/17179401/af46c8fb-cb33-483a-b5c1-9142843da2bd 192 | 193 |
194 | 195 | 196 |
197 | ADB access over QSRN 198 | 199 | https://user-images.githubusercontent.com/17179401/216651601-6ddc8ddf-7248-4c2b-bd77-00f00f773c80.mov 200 | 201 |
202 | --------------------------------------------------------------------------------