-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement the
rbmk nc
subcommand
This subcommand emulates a subset of the OpenBSD nc(1) command line utility and allows to measure TCP/TLS endpoints.
- Loading branch information
1 parent
dbde41f
commit 88ed772
Showing
6 changed files
with
476 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
# rbmk nc - TCP/TLS Client | ||
|
||
## Usage | ||
|
||
``` | ||
rbmk nc [flags] HOST PORT | ||
``` | ||
|
||
## Description | ||
|
||
The `rbmk nc` command emulates a subset of the OpenBSD `nc(1)` command, | ||
including connecting to remote TCP/TLS endpoints, scanning for open ports, | ||
sending and receiving data over the network. | ||
|
||
The `HOST` may be a domain name, an IPv4 address, or an IPv6 address. When | ||
using a domain name, we use the system resolver to resolve the name to a | ||
list of IP addresses and try all of them until one succeeds. For measuring, | ||
it is recommended to specify an IP address directly. | ||
|
||
## Flags | ||
|
||
### `--alpn PROTO` | ||
|
||
Specify ALPN protocol(s) for TLS connections. Can be specified | ||
multiple times to support protocol negotiation. For example: | ||
|
||
--alpn h2 --alpn http/1.1 | ||
|
||
Must be used alongside the `--tls` flag. | ||
|
||
### `-c, --tls` | ||
|
||
Perform a TLS handshake after a successful TCP connection. | ||
|
||
### `-h, --help` | ||
|
||
Print this help message. | ||
|
||
### `--logs FILE` | ||
|
||
Writes structured logs to the given FILE. If FILE already exists, we | ||
append to it. If FILE does not exist, we create it. If FILE is a single | ||
dash (`-`), we write to the stdout. If you specify `--logs` multiple | ||
times, we write to the last FILE specified. | ||
|
||
### `--measure` | ||
|
||
Do not exit with `1` if communication with the server fails. Only exit | ||
with `1` in case of usage errors, or failure to process inputs. You should | ||
use this flag inside measurement scripts along with `set -e`. Errors are | ||
still printed to stderr along with a note indicating that the command is | ||
continuing due to this flag. | ||
|
||
### `--sni SERVER_NAME` | ||
|
||
Specify the server name for the SNI extension in the TLS | ||
handshake. For example: | ||
|
||
--sni www.example.com | ||
|
||
Must be used alongside the `--tls` flag. | ||
|
||
### `-v` | ||
|
||
Print more verbose output. | ||
|
||
### `-w, --timeout TIMEOUT` | ||
|
||
Time-out I/O operations (connect, recv, send) after | ||
a `TIMEOUT` number of seconds. | ||
|
||
### `-z, --scan` | ||
|
||
Without `--tls`, perform a port scan and report whether the | ||
remote port is open. With `--tls`, perform a TLS handshake | ||
and then close the remote connection. | ||
|
||
## Examples | ||
|
||
Basic TCP connection to HTTP port: | ||
|
||
``` | ||
$ rbmk nc example.com 80 | ||
``` | ||
|
||
TLS connection with HTTP/2 and HTTP/1.1 ALPN: | ||
|
||
``` | ||
$ rbmk nc -c --alpn h2 --alpn http/1.1 example.com 443 | ||
``` | ||
|
||
Check if port is open (scan mode) with a five seconds timeout: | ||
|
||
``` | ||
$ rbmk nc -z -w5 example.com 80 | ||
``` | ||
|
||
Same as above but also perform a TLS handshake: | ||
|
||
``` | ||
$ rbmk nc --alpn h2 --alpn http/1.1 -z -c -w5 example.com 443 | ||
``` | ||
|
||
Saving structured logs: | ||
|
||
``` | ||
$ rbmk nc --logs conn.jsonl example.com 80 | ||
``` | ||
|
||
## Exit Status | ||
|
||
The nc utility exits with `0` on success and `1` on error. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
// Package nc implements the `rbmk nc` command. | ||
package nc | ||
|
||
import ( | ||
"context" | ||
_ "embed" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"os" | ||
"time" | ||
|
||
"github.com/rbmk-project/common/cliutils" | ||
"github.com/rbmk-project/common/closepool" | ||
"github.com/rbmk-project/rbmk/internal/markdown" | ||
"github.com/spf13/pflag" | ||
) | ||
|
||
//go:embed README.md | ||
var readme string | ||
|
||
func NewCommand() cliutils.Command { | ||
return command{} | ||
} | ||
|
||
type command struct{} | ||
|
||
// Help implements [cliutils.Command]. | ||
func (cmd command) Help(env cliutils.Environment, argv ...string) error { | ||
fmt.Fprintf(env.Stdout(), "%s\n", markdown.MaybeRender(readme)) | ||
return nil | ||
} | ||
|
||
// Main implements [cliutils.Command]. | ||
func (cmd command) Main(ctx context.Context, env cliutils.Environment, argv ...string) error { | ||
// 1. honour requests for printing the help | ||
if cliutils.HelpRequested(argv...) { | ||
return cmd.Help(env, argv...) | ||
} | ||
|
||
// 2. parse command line flags | ||
clip := pflag.NewFlagSet("rbmk nc", pflag.ContinueOnError) | ||
|
||
// Core netcat flags (OpenBSD compatible) | ||
useTLS := clip.BoolP("tls", "c", false, "use TLS") | ||
verbose := clip.BoolP("verbose", "v", false, "verbose output") | ||
wait := clip.IntP("wait", "w", 0, "timeout for connect, send, and recv") | ||
scan := clip.BoolP("zero", "z", false, "scan for listening daemons") | ||
|
||
// Additional TLS features | ||
alpn := clip.StringSlice("alpn", nil, "TLS ALPN protocol(s)") | ||
sni := clip.String("sni", "", "TLS SNI server name") | ||
|
||
// RBMK specific flags | ||
logfile := clip.String("logs", "", "write structured logs to file") | ||
measure := clip.Bool("measure", false, "do not exit 1 on measurement failure") | ||
|
||
if err := clip.Parse(argv[1:]); err != nil { | ||
fmt.Fprintf(env.Stderr(), "rbmk nc: %s\n", err.Error()) | ||
fmt.Fprintf(env.Stderr(), "Run `rbmk nc --help` for usage.\n") | ||
return err | ||
} | ||
|
||
// 3. validate arguments | ||
args := clip.Args() | ||
if len(args) != 2 { | ||
err := errors.New("expected host and port arguments") | ||
fmt.Fprintf(env.Stderr(), "rbmk nc: %s\n", err.Error()) | ||
fmt.Fprintf(env.Stderr(), "Run `rbmk nc --help` for usage.\n") | ||
return err | ||
} | ||
host, port := args[0], args[1] | ||
|
||
// 4. setup task with defaults | ||
task := &Task{ | ||
ALPNProtocols: *alpn, | ||
Host: host, | ||
LogsWriter: io.Discard, | ||
Port: port, | ||
ScanMode: *scan, | ||
ServerName: host, | ||
Stderr: io.Discard, | ||
Stdin: env.Stdin(), | ||
Stdout: env.Stdout(), | ||
UseTLS: *useTLS, | ||
WaitTimeout: 0, | ||
} | ||
|
||
// 5. finish setting up the task | ||
if *sni != "" { | ||
task.ServerName = *sni | ||
} | ||
if *wait > 0 { | ||
task.WaitTimeout = time.Second * time.Duration(*wait) | ||
} | ||
if *verbose { | ||
task.Stderr = env.Stderr() | ||
} | ||
|
||
// 6. handle logs flag | ||
var filepool closepool.Pool | ||
switch *logfile { | ||
case "": | ||
// nothing | ||
case "-": | ||
task.LogsWriter = env.Stdout() | ||
default: | ||
filep, err := os.OpenFile(*logfile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600) | ||
if err != nil { | ||
err = fmt.Errorf("cannot open log file: %w", err) | ||
fmt.Fprintf(env.Stderr(), "rbmk nc: %s\n", err.Error()) | ||
return err | ||
} | ||
filepool.Add(filep) | ||
task.LogsWriter = io.MultiWriter(task.LogsWriter, filep) | ||
} | ||
|
||
// 7. run the task | ||
err := task.Run(ctx) | ||
if err != nil && *measure { | ||
fmt.Fprintf(env.Stderr(), "rbmk nc: %s\n", err.Error()) | ||
fmt.Fprintf(env.Stderr(), "rbmk nc: not failing because you specified --measure\n") | ||
err = nil | ||
} | ||
|
||
// 8. ensure we close the opened files | ||
if err2 := filepool.Close(); err2 != nil { | ||
fmt.Fprintf(env.Stderr(), "rbmk nc: %s\n", err2.Error()) | ||
return err2 | ||
} | ||
|
||
// 9. handle error when running the task | ||
if err != nil { | ||
fmt.Fprintf(env.Stderr(), "rbmk nc: %s\n", err.Error()) | ||
return err | ||
} | ||
return nil | ||
} |
Oops, something went wrong.