Skip to content

Commit

Permalink
feat: implement the rbmk nc subcommand
Browse files Browse the repository at this point in the history
This subcommand emulates a subset of the OpenBSD nc(1)
command line utility and allows to measure TCP/TLS endpoints.
  • Loading branch information
bassosimone committed Dec 9, 2024
1 parent dbde41f commit 88ed772
Show file tree
Hide file tree
Showing 6 changed files with 476 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ you can observe each operation in isolation.
- Core Measurement Commands:
- `dig`: DNS measurements with multiple protocols
- `curl`: HTTP(S) endpoint measurements
- `nc`: TCP/TLS endpoint measurements
- `stun`: Resolve the public IP addresses

- Scripting Support:
Expand Down Expand Up @@ -110,6 +111,7 @@ $ rbmk tutorial
Core Measurement Commands:
- `curl`: Measures HTTP/HTTPS endpoints with `curl(1)`-like syntax.
- `dig`: Performs DNS measurements with `dig(1)`-like syntax.
- `nc` - Measures TCP and TLS endpoints with an OpenBSD `nc(1)`-like syntax.
- `stun`: Resolves the public IP addresses using STUN.

Unix-like Commands for Scripting:
Expand Down
1 change: 1 addition & 0 deletions pkg/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ to facilitate network exploration and measurements.

* `curl` - Measures HTTP/HTTPS endpoints with `curl(1)`-like syntax.
* `dig` - Performs DNS measurements with `dig(1)`-like syntax.
* `nc` - Measures TCP and TLS endpoints with an OpenBSD `nc(1)`-like syntax.
* `stun` - Performs STUN binding requests to discover public IP address.

### Unix-like Commands for Scripting
Expand Down
2 changes: 2 additions & 0 deletions pkg/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/rbmk-project/rbmk/pkg/cli/ipuniq"
"github.com/rbmk-project/rbmk/pkg/cli/mkdir"
"github.com/rbmk-project/rbmk/pkg/cli/mv"
"github.com/rbmk-project/rbmk/pkg/cli/nc"
"github.com/rbmk-project/rbmk/pkg/cli/pipe"
"github.com/rbmk-project/rbmk/pkg/cli/rm"
"github.com/rbmk-project/rbmk/pkg/cli/sh"
Expand All @@ -40,6 +41,7 @@ func NewCommand() cliutils.Command {
"ipuniq": ipuniq.NewCommand(),
"mkdir": mkdir.NewCommand(),
"mv": mv.NewCommand(),
"nc": nc.NewCommand(),
"pipe": pipe.NewCommand(),
"rm": rm.NewCommand(),
"sh": sh.NewCommand(),
Expand Down
112 changes: 112 additions & 0 deletions pkg/cli/nc/README.md
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.
140 changes: 140 additions & 0 deletions pkg/cli/nc/nc.go
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
}
Loading

0 comments on commit 88ed772

Please sign in to comment.