telnet

package module
v0.0.0-...-064adab Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 12, 2024 License: MIT Imports: 13 Imported by: 0

README

telnet-go

GoDoc

A Telnet server and client library implementation written in Go. Heavily inspired by the standard library's net/http implementation.

Forked from https://github.com/reiver/go-telnet.

Get Started

The telnet-go library provides low level interfaces for interacting with Telnet data streams, either as a server or client.

There's also the shell package, which provides a simple Telnet shell server example.

Setup a Telnet Server

Basic Server

Before we can host a server, we need to specify a handler. We provide a sample handler as telnet.EchoHandler to illustrate how a handler should be written. This handler echoes whatever the client sends, back to the client.

The following code will serve this handler on your system's primary IP, using port 23 (Telnet's standard port).

package main

import (
	"github.com/GlobalCyberAlliance/telnet-go"
)

func main() {
	if err := telnet.ListenAndServe("0.0.0.0:23", telnet.EchoHandler); err != nil {
		panic(err)
	}
}

The echo handler simply echoes client text back to the client upon submission. This is what the client would see:

~/Projects/globalcyberalliance/telnet-go
❯ telnet localhost
Trying ::1...
Connected to localhost.
Escape character is '^]'.
root
root
exit
exit
^]
telnet> quit
Connection closed.
Shell Server

A common use for Telnet is to act as a shell server (similar to SSH). We provide a simple package that showcases how to handle user auth, and how you might want to handle commands.

package main

import (
	"log"

	"github.com/GlobalCyberAlliance/telnet-go"
	"github.com/GlobalCyberAlliance/telnet-go/shell"
)

func main() {
	authHandler := shell.NewAuthHandler("root", "password", 3)
	commands := []shell.Command{
		{
			Regex:    "^docker$",
			Response: "\nUsage:  docker [OPTIONS] COMMAND\r\n",
		},
		{
			Regex:    "^docker .*$",
			Response: "Error response from daemon: dial unix docker.raw.sock: connect: connection refused\r\n",
		},
		{
			Regex:    "^uname$",
			Response: "Linux\r\n",
		},
	}

	srv := shell.Server{AuthHandler: authHandler, Commands: commands}}

	if err := telnet.ListenAndServe("0.0.0.0:23", srv.HandlerFunc); err != nil {
		log.Fatal(err)
	}
}

This will serve a simple shell server that accepts root and password as the username and password respectively, and will permit 3 auth attempts.

By default, the Command object exposed here accepts regex, and a single string response. This interface is sufficient for a simple shell interface; however, you can instead use the GenericHandler to manually handle this process yourself. Here's what that might look like:

package main

import (
	"log"
	"strings"

	"github.com/GlobalCyberAlliance/telnet-go"
	"github.com/GlobalCyberAlliance/telnet-go/shell"
)

func main() {
	authHandler := shell.NewAuthHandler("root", "password", 2)

	srv := shell.Server{AuthHandler: authHandler, GenericHandler: func(command string) string {
		fields := strings.Fields(command)
		if len(fields) == 0 {
			return "missing command\r\n"
		}

		switch fields[0] {
		case "docker":
			return "\nUsage:  docker [OPTIONS] COMMAND\n"
		}

		return fields[0] + ": command not found\r\n"
	}}

	if err := telnet.ListenAndServe("0.0.0.0:23", srv.HandlerFunc); err != nil {
		log.Fatal(err)
	}
}
Creating a Handler

Here's a simple handler. You can write directly to the io.Writer and read from the io.Reader; however, we provide a few functions to handle this for you (telnet.WriteLine and telnet.ReadLine respectively).

This handler will write Welcome! to the user, and will await their input. If they send a blank response, the server will close the connection after writing Goodbye!. If the user enters anything else, it'll simply echo back You wrote: whatever the user entered.

package main

import (
	"context"
	"io"

	"github.com/GlobalCyberAlliance/telnet-go"
)

func main() {
	if err := telnet.ListenAndServe("0.0.0.0:23", YourHandlerFunc); err != nil {
		panic(err)
	}
}

func YourHandlerFunc(ctx context.Context, w io.Writer, r io.Reader) {
	if err := telnet.WriteLine(w, "Welcome!\n"); err != nil {
		return
	}

	for {
		line, err := telnet.ReadLine(r)
		if err != nil {
			return
		}

		if len(line) == 0 {
			if err = telnet.WriteLine(w, "Goodbye!\n"); err != nil {
				return
			}
			return
		}

		if err = telnet.WriteLine(w, "You wrote: "+line+"\n"); err != nil {
			return
		}
	}
}
Issuing a Telnet Command

You may need to issue a Telnet command to your client. By default, the telnet.writer interprets IAC (255) as IAC IAC (255, 255), as Telnet requires this for data streams. To workaround this, we expose a telnet.WriteCommand function. This function prepends telnet.commandSignature() to the beginning of the byte slice, to signal to the internal telnet.writer.Write() function to not escape the upcoming IAC (255) byte.

We make use of this in the shell.AuthHandler to mask the user's password during the login process:

if err = telnet.WriteLine(w, "Password: "); err != nil {
    return false
}

// Enable ECHO to hide the user password.
if _, err = telnet.WriteCommand(w, telnet.IAC, telnet.WILL, telnet.ECHO); err != nil {
    return false
}

userPassword, err := telnet.ReadLine(r)
if err != nil {
    return false
}

// Disable ECHO.
if _, err = telnet.WriteCommand(w, telnet.IAC, telnet.WONT, telnet.ECHO); err != nil {
    return false
}

Setup a Telnet Client

Similarly to setting up a server, before we open a client connection we need to specify a caller. We provide a sample caller as telnet.EchoCaller to illustrate how a caller should be written. This caller sends data from os.Stdin to the given Telnet server, and echoes back the server's response to os.Stdout.

The following code will use this caller to handle calling a Telnet server listening on the localhost. The Telnet server in our example is a simple handler that repeats the client's submission back to them, with You wrote: prepended.

package main

import (
	"github.com/GlobalCyberAlliance/telnet-go"
)

func main() {
	if err := telnet.DialAndCall("localhost:23", telnet.EchoCaller); err != nil {
		panic(err)
	}
}

This is what the client would see:

Welcome!
hello
You wrote: hello
this is a test
You wrote: this is a test

Note: The stock telnet.EchoCaller uses the telnet.ReadLine function, which isn't ideal for interacting with a server's response. It relies on the server ending its data stream with a newline; however, the server may not do this (for example, if it's sending an auth prompt).

Notes

This fork refactored a lot of the original author's codebase to have a cleaner and easier to use API. We required the server implementation internally, so the client implementation still needs some work (as we didn't really need it).

Documentation

Index

Constants

View Source
const (
	ECHO     byte = 1
	SGA      byte = 3
	NL       byte = 10 // New line.
	CR       byte = 13 // Carriage return.
	LINEMODE byte = 34
	SE       byte = 240
	SB       byte = 250
	WILL     byte = 251
	WONT     byte = 252
	DO       byte = 253
	DONT     byte = 254
	IAC      byte = 255
)

Variables

This section is empty.

Functions

func DialAndCall

func DialAndCall(srvAddr string, caller Caller) error

func DialAndCallTLS

func DialAndCallTLS(srvAddr string, caller Caller, tlsConfig *tls.Config) error

func ListenAndServe

func ListenAndServe(addr string, handler HandlerFunc) error

ListenAndServe listens on the TCP network address 'addr' and then spawns a call to ServeTELNET method on 'handler' to serve each incoming connection.

func ListenAndServeTLS

func ListenAndServeTLS(addr string, certFile string, keyFile string, handler HandlerFunc) error

ListenAndServeTLS functions similarly to ListenAndServe, but supports the TELNET protocol over TLS.

This enables 'secured telnet' (TELNETS), typically on port 992 by default, though this can be overridden using the 'addr' argument.

func LongWrite

func LongWrite(w io.Writer, p []byte) (int64, error)

LongWrite attempts to write the bytes from 'p' to the writer 'w', handling short writes where w.Write returns io.ErrShortWrite and n < len(p).

func ReadLine

func ReadLine(reader io.Reader) (string, error)

ReadLine is a helper function to read a line from the Telnet client.

This doesn't really work for reading from servers, as servers may not finish a line with a \r or \n (e.g. an auth prompt), causing reader.Read(p) to block indefinitely.

func Serve

func Serve(listener net.Listener, handler HandlerFunc) error

Serve accepts an incoming TELNET or TELNETS client connection on the net.Listener 'listener'.

func WriteCommand

func WriteCommand(writer io.Writer, command byte, option byte, action byte) (n int, err error)

WriteCommand is a dirty workaround to write Telnet commands directly to the client. The internal wrapper satisfies io.Write, preventing us from including custom logic to handle commands (without risking bodging real data). Instead, this submits a signature (IAC x4) the underlying Write function knows to look for, and to treat as a command.

func WriteLine

func WriteLine(writer io.Writer, text ...string) error

Types

type Caller

type Caller interface {
	CallTELNET(context.Context, io.Writer, io.Reader)
}

A Caller represents the client end of a TELNET (or TELNETS) connection.

Writing data to the Writer passed as an argument to the CallTELNET method will send data to the TELNET (or TELNETS) server.

Reading data from the Reader passed as an argument to the CallTELNET method will receive data from the TELNET server.

The Writer's Write method sends "escaped" TELNET (and TELNETS) data.

The Reader's Read method "un-escapes" TELNET (and TELNETS) data, and filters out TELNET (and TELNETS) command sequences.

var StandardCaller Caller = internalStandardCaller{}

StandardCaller is a simple TELNET client which sends to the server any data it gets from os.Stdin as TELNET (and TELNETS) data, and writes any TELNET (or TELNETS) data it receives from the server to os.Stdout, and writes any error it has to os.Stderr.

type CallerFunc

type CallerFunc func(context.Context, io.Writer, io.Reader)

The CallerFunc type is an adapter to allow the use of ordinary functions as TELNET callers.

var EchoCaller CallerFunc = func(ctx context.Context, w io.Writer, r io.Reader) {
	for {
		serverLine, err := ReadLine(r)
		if err != nil {
			if err == io.EOF {
				fmt.Println("Connection closed by foreign host.")
				return
			}

			fmt.Printf("Failed to read from the server: %v\n", err)
			return
		}

		if _, err = os.Stdout.WriteString(serverLine); err != nil {
			fmt.Printf("Failed to write server response to stdout: %v\n", err)
			return
		}

		clientLine, err := ReadLine(os.Stdin)
		if err != nil {
			fmt.Printf("Failed to read client input: %v\n", err)
			return
		}

		if !strings.HasSuffix(clientLine, "\r\n") {

			clientLine = strings.TrimSuffix(clientLine, "\n") + "\r\n"
		}

		if err = WriteLine(w, clientLine); err != nil {
			fmt.Printf("Failed to write to server: %v\n", err)
			return
		}
	}
}

EchoCaller is a simple TELNET client which sends to the server any data it gets from os.Stdin as TELNET data, and writes any TELNET data it receives from the server to os.Stdout.

func (CallerFunc) CallTELNET

func (f CallerFunc) CallTELNET(ctx context.Context, w io.Writer, r io.Reader)

CallTELNET calls f(ctx, w, r).

type Client

type Client struct {
	Caller Caller
	Logger *slog.Logger
}

func NewClient

func NewClient(caller Caller, logger *slog.Logger) *Client

func (*Client) Call

func (client *Client) Call(conn *Conn) error

type Conn

type Conn struct {
	// contains filtered or unexported fields
}

func Dial

func Dial(protocol, addr string) (*Conn, error)

Dial makes an unsecured TELNET client connection to the specified address. If no address is supplied, it'll default to localhost.

func DialTLS

func DialTLS(protocol, addr string, tlsConfig *tls.Config) (*Conn, error)

DialTLS makes a secure TELNETS client connection to the specified address. If no address is supplied, it'll default to localhost.

func (*Conn) Close

func (c *Conn) Close() error

Close closes the client connection.

func (*Conn) LocalAddr

func (c *Conn) LocalAddr() net.Addr

LocalAddr returns the local network address.

func (*Conn) Read

func (c *Conn) Read(p []byte) (int, error)

Read reads bytes from the server into p.

func (*Conn) RemoteAddr

func (c *Conn) RemoteAddr() net.Addr

RemoteAddr returns the remote network address.

func (*Conn) Write

func (c *Conn) Write(p []byte) (int, error)

Write writes bytes to the server from p.

type HandlerFunc

type HandlerFunc func(context.Context, io.Writer, io.Reader)

The HandlerFunc type is an adapter to allow the use of ordinary functions as TELNET handlers.

var EchoHandler HandlerFunc = func(ctx context.Context, w io.Writer, r io.Reader) {
	// Buffer needs to be small to avoid waiting for it to fill up.
	var buffer [1]byte
	p := buffer[:]

	for {
		n, err := r.Read(p)
		if n > 0 {
			if _, err := w.Write(p[:n]); err != nil {
				return
			}
		}

		if err != nil {
			break
		}
	}
}

EchoHandler is a simple TELNET server which "echos" back to the client any (non-command) data back to the TELNET client, it received from the TELNET client.

func (HandlerFunc) ServeTELNET

func (f HandlerFunc) ServeTELNET(ctx context.Context, w io.Writer, r io.Reader)

ServeTELNET calls f(ctx, w, r).

type Server

type Server struct {
	Handler   HandlerFunc // handler to invoke; default is telnet.EchoHandler if nil
	TLSConfig *tls.Config // optional TLS configuration; used by ListenAndServeTLS

	Addr string // TCP address to listen on; ":telnet" or ":telnets" if empty (used with ListenAndServe or ListenAndServeTLS respectively).
	// contains filtered or unexported fields
}

Server defines parameters of a running TELNET server.

func (*Server) ListenAndServe

func (server *Server) ListenAndServe() error

ListenAndServe listens on the TCP network address 'server.Addr' and then spawns a call to Serve method on 'server.Handler' to serve each incoming connection.

func (*Server) ListenAndServeTLS

func (server *Server) ListenAndServeTLS(certFile string, keyFile string) error

ListenAndServeTLS behaves similarly to ListenAndServe, but operates over the TELNET protocol with TLS encryption.

In the context of the TELNET protocol, it enables 'secured telnet' (TELNETS), typically on port 992.

func (*Server) Serve

func (server *Server) Serve(listener net.Listener) error

Serve accepts an incoming TELNET client connection on the net.Listener 'listener'.

func (*Server) SetLogger

func (server *Server) SetLogger(logger *slog.Logger)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL