From 4f8ef2757dfdc0361b1fc7e7a3b3e65a0986e1c6 Mon Sep 17 00:00:00 2001 From: Simon Marsh Date: Tue, 27 Sep 2022 13:46:36 +0100 Subject: [PATCH] initial release --- .gitignore | 1 + go.mod | 7 + go.sum | 15 + modemulator.go | 818 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 841 insertions(+) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 modemulator.go diff --git a/.gitignore b/.gitignore index 4397a09..ed34bb3 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,4 @@ flycheck_*.el # Go workspace file go.work +modemulator diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..beb653d --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module modemulator + +go 1.19 + +require github.com/sirupsen/logrus v1.9.0 + +require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ed65537 --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/modemulator.go b/modemulator.go new file mode 100644 index 0000000..85551bc --- /dev/null +++ b/modemulator.go @@ -0,0 +1,818 @@ +////////////////////////////////////////////////////////////////////////// + +package main + +////////////////////////////////////////////////////////////////////////// + +import ( + "bytes" + "errors" + "fmt" + log "github.com/sirupsen/logrus" + "io" + "net" + "os" + "os/signal" + "regexp" + "strconv" + "strings" + "time" +) + +////////////////////////////////////////////////////////////////////////// +// some config options + +const PPS_TARGET = 10 + +const SERVER_BIND = "" +const SERVER_PORT_INDEX = 10000 + +const TELNET_SERVER = "localhost" + +var BAUDS []uint = []uint{ + 300, + 1200, + 2400, + 4800, + 9600, + 14400, + 19200, + 28800, + 33600, + 56000, + 115000, +} + +var AddressBook map[string]string = map[string]string{ + "54311": "shell.fr-par1.burble.dn42:23", + "42": "localhost:23", +} + +var OnlyNumbers *regexp.Regexp = regexp.MustCompile("([^0-9]+)") + +// global data and structures + +var Listeners []net.Listener + +type TCode uint + +const ( + TN_NORM TCode = 65535 + TN_IAC TCode = 255 + TN_DONT TCode = 254 + TN_DO TCode = 253 + TN_WONT TCode = 252 + TN_WILL TCode = 251 + TN_SB TCode = 250 + TN_GA TCode = 249 + TN_EL TCode = 248 + TN_EC TCode = 247 + TN_AYT TCode = 246 + TN_AO TCode = 245 + TN_IP TCode = 244 + TN_BRK TCode = 243 + TN_DM TCode = 242 + TN_NOP TCode = 241 + TN_SE TCode = 240 +) + +type DTEMode uint + +const ( + DTE_COMMAND DTEMode = iota + DTE_COMMAND_A + DTE_COMMAND_AT + DTE_ATTN_1 + DTE_ATTN_2 + DTE_ATTN_3 + DTE_CONNECTED +) + +type Modem struct { + baud uint + dce net.Conn + mode DTEMode + echo bool + cmdBuff *bytes.Buffer + dteBuff []byte + connected bool + dte net.Conn + state TCode + readSlot time.Time + writeSlot time.Time + dceBuff []byte +} + +var Modems map[string]*Modem + +////////////////////////////////////////////////////////////////////////// +// utility function to set the log level + +func setLogLevel(levelStr string) { + + if level, err := log.ParseLevel(levelStr); err != nil { + // failed to set the level + + // set a sensible default and, of course, log the error + log.SetLevel(log.InfoLevel) + log.WithFields(log.Fields{ + "loglevel": levelStr, + "error": err, + }).Error("Failed to set requested log level") + + } else { + + // set the requested level + log.SetLevel(level) + + } +} + +////////////////////////////////////////////////////////////////////////// +// listen on required ports + +func Listen() { + + for _, baud := range BAUDS { + port := SERVER_PORT_INDEX + (baud / 100) + bind := SERVER_BIND + ":" + strconv.Itoa(int(port)) + + log.WithFields(log.Fields{ + "bind": bind, + "baud": baud, + }).Debug("Listening on socket") + + listener, err := net.Listen("tcp", bind) + if err != nil { + log.WithFields(log.Fields{ + "bind": bind, + "baud": baud, + "error": err, + }).Fatal("Failed to bind listening port") + } + + Listeners = append(Listeners, listener) + + // spin off a thread waiting for new connections + go func(p uint, b uint) { + for { + + conn, err := listener.Accept() + if err != nil { + if errors.Is(err, net.ErrClosed) { + log.WithFields(log.Fields{ + "port": p, + }).Debug("Listener closed") + return + } else { + log.WithFields(log.Fields{ + "port": p, + "error": err, + }).Error("Listen Error") + return + } + } + + // initialise modem and start listening for commands + modem := &Modem{} + go modem.DTEReceive(b, conn) + } + }(port, baud) + } + +} + +////////////////////////////////////////////////////////////////////////// +// modem utility functions + +func (m *Modem) disconnect() { + if m.connected { + m.dce.Close() + } + m.connected = false + m.mode = DTE_COMMAND +} + +func (m *Modem) Shutdown() { + m.disconnect() + m.dte.Close() +} + +func (m *Modem) bytesPerTimeslot() uint { + return (m.baud / (PPS_TARGET * 9)) + 1 +} + +func (m *Modem) createBuffers() { + m.dteBuff = make([]byte, m.baud) + m.dceBuff = make([]byte, m.bytesPerTimeslot()) + if m.cmdBuff == nil { + m.cmdBuff = new(bytes.Buffer) + m.cmdBuff.Grow(80) + } +} + +func (m *Modem) dteWrite(data []byte) { + if _, err := m.dte.Write(data); err != nil { + m.Shutdown() + } +} + +func (m *Modem) dceWrite(data []byte) { + + // step through the data in chunks + csize := int(m.bytesPerTimeslot()) + for scan := 0; scan < len(data); { + + // wait if necessary until we can send + now := time.Now() + if m.writeSlot.After(now) { + wtime := m.writeSlot.Sub(now) + fmt.Printf("waiting %d for write slot\n", wtime) + time.Sleep(wtime) + } + + // send the data + end := scan + csize + if end > len(data) { + end = len(data) + } + chunk := data[scan:end] + if _, err := m.dce.Write(chunk); err != nil { + m.disconnect() + m.noCarrier() + return + } + scan += len(chunk) + + // set the next available timeslot + tlen := time.Duration((9 * 1000 * 1000 * 1000 * len(chunk)) / int(m.baud)) + fmt.Printf("%d chars, next write slot in %d\n", len(chunk), tlen) + m.writeSlot = now.Add(tlen) + } + +} + +func (m *Modem) tnWrite(codes []TCode) { + b := make([]byte, len(codes)) + for i, c := range codes { + b[i] = byte(c) + } + m.dceWrite(b) +} + +func (m *Modem) cmdOK() { + m.dteWrite([]byte("OK\r\n")) +} + +func (m *Modem) cmdError() { + m.dteWrite([]byte("ERROR\r\n")) +} + +func (m *Modem) noCarrier() { + m.dteWrite([]byte("NO CARRIER\r\n")) +} + +func (m *Modem) connect(dial string) error { + + endpoint, found := AddressBook[dial] + if !found { + return errors.New("Address not found:" + dial) + } + + conn, err := net.Dial("tcp", endpoint) + if err != nil { + log.WithFields(log.Fields{ + "error": err, + }).Error("Telnet connection failed") + m.connected = false + return err + } + m.dce = conn + m.connected = true + m.mode = DTE_CONNECTED + return nil +} + +////////////////////////////////////////////////////////////////////////// +// Modem receive thread + +func (m *Modem) DTEReceive(baud uint, dte net.Conn) { + + remote := dte.RemoteAddr().String() + log.WithFields(log.Fields{ + "remote": remote, + }).Info("Accepting new modem") + + // initialise state + m.baud = baud + m.dte = dte + m.createBuffers() + m.atCmd("z") + + // register + Modems[remote] = m + defer func() { + log.WithFields(log.Fields{ + "remote": remote, + }).Debug("Shutting down modem") + m.Shutdown() + delete(Modems, remote) + }() + + for { + + // read some data + + if m.mode == DTE_ATTN_3 { + // potential hangup, set a read deadline + m.dte.SetReadDeadline(time.Now().Add(500 * time.Millisecond)) + } + + // do the read + available, err := m.dte.Read(m.dteBuff) + + if m.mode == DTE_ATTN_3 { + // clear the deadline and return to connected state + m.dte.SetReadDeadline(time.Time{}) + + if (err != nil) && errors.Is(err, os.ErrDeadlineExceeded) { + // back to command mode + m.mode = DTE_COMMAND + m.cmdOK() + err = nil + + } else { + // no attention + m.mode = DTE_CONNECTED + m.dce.Write([]byte("+++")) + } + } + + if err != nil { + if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { + log.WithFields(log.Fields{ + "remote": remote, + }).Info("Modem DTE closed") + return + } else { + log.WithFields(log.Fields{ + "remote": remote, + "error": err, + }).Error("DTE Read error") + return + } + } + + for scan := 0; scan < available; { + switch m.mode { + + case DTE_COMMAND: + // waiting for AT + + start := scan + for ; scan < available; scan++ { + if m.dteBuff[scan] == 'a' || m.dteBuff[scan] == 'A' { + m.mode = DTE_COMMAND_A + scan++ + break + } + } + + // echo characters if required + if m.echo { + m.dteWrite(m.dteBuff[start:scan]) + } + + case DTE_COMMAND_A: + // have A, is next byte a T ? + if m.dteBuff[scan] == 't' || m.dteBuff[scan] == 'T' { + // yes, start AT command mode + m.mode = DTE_COMMAND_AT + scan++ + + // echo char if needed + if m.echo { + m.dteWrite(m.dteBuff[scan-1 : scan]) + } + + } else { + // not a T, reset the mode + m.mode = DTE_COMMAND + } + + case DTE_COMMAND_AT: + // have AT, waiting for \r + + eol := false + start := scan + for ; scan < available; scan++ { + if m.dteBuff[scan] == '\r' { + // signal EOL + eol = true + break + } + } + + data := m.dteBuff[start:scan] + + // echo characters if required + if m.echo { + m.dteWrite(data) + } + + // add to command buffer + if (m.cmdBuff.Len() + len(data)) > 80 { + // nah too long + m.cmdBuff.Reset() + m.cmdError() + } else { + m.cmdBuff.Write(data) + } + + // parse AT command if EOL + if eol { + scan++ + m.dteWrite([]byte("\r\n")) + + cmd := m.cmdBuff.String() + log.WithFields(log.Fields{ + "remote": remote, + "cmd": cmd, + }).Debug("ATCMD") + + m.atCmd(cmd) + m.cmdBuff.Reset() + if m.mode != DTE_CONNECTED { + m.mode = DTE_COMMAND + } + } + + case DTE_CONNECTED: + // stream to DCE + + start := scan + for ; scan < available; scan++ { + if m.dteBuff[scan] == '+' { + m.mode = DTE_ATTN_1 + break + } + } + + // write data so far + data := m.dteBuff[start:scan] + m.dceWrite(data) + + if scan < available { + // + was received, skip over it + scan++ + } + + case DTE_ATTN_1: + if m.dteBuff[scan] == '+' { + m.mode = DTE_ATTN_2 + scan++ + } else { + // no attention, send suitable number of + + m.mode = DTE_CONNECTED + m.dceWrite([]byte("+")) + } + + case DTE_ATTN_2: + if m.dteBuff[scan] == '+' { + + scan++ + if scan < available { + // more bytes were available, no hangup + m.mode = DTE_CONNECTED + m.dceWrite([]byte("+++")) + } else { + // wait for pause + m.mode = DTE_ATTN_3 + } + + } else { + // no attention, send suitable number of + + m.mode = DTE_CONNECTED + m.dceWrite([]byte("++")) + } + } + } + } +} + +////////////////////////////////////////////////////////////////////////// +// action AT command + +func (m *Modem) atCmd(cmd string) { + cmd = strings.ToLower(strings.TrimSpace(cmd)) + l := len(cmd) + + // cope with zero length commands + if l == 0 { + m.cmdError() + return + } + + switch cmd[0] { + case 'd': + // dial + + if m.connected || l < 4 { + m.cmdError() + return + } + + // remove any non-digit characters + dial := OnlyNumbers.ReplaceAllString(cmd, "") + log.WithFields(log.Fields{ + "dial": dial, + }).Debug("Dial number") + + if err := m.connect(dial); err != nil { + m.noCarrier() + return + } + + // show connect string + cstr := "CONNECT " + strconv.Itoa(int(m.baud)) + "\r\n" + m.dte.Write([]byte(cstr)) + + // start dce read thread + go m.dceTelnet() + + case 'h': + // hangup + switch { + case l == 1 || cmd[1] == '0': + m.disconnect() + m.cmdOK() + default: + m.cmdError() + } + + case 'e': + // enable/disable echo + switch { + case l == 1 || cmd[1] == '0': + m.echo = false + m.cmdOK() + case cmd[1] == '1': + m.echo = true + m.cmdOK() + default: + m.cmdError() + } + + case 'o': + if l == 1 { + if m.connected { + m.mode = DTE_CONNECTED + m.cmdOK() + } else { + m.noCarrier() + } + } else { + m.cmdError() + } + + case 'z': + if l == 1 || cmd[1] == '0' { + m.disconnect() + m.echo = true + m.mode = DTE_COMMAND + m.cmdBuff.Reset() + m.cmdOK() + } else { + m.cmdError() + } + + default: + m.cmdOK() + } + +} + +////////////////////////////////////////////////////////////////////////// +// DCE read thread + +func (m *Modem) dceTelnet() { + + defer func() { + log.WithFields(log.Fields{ + "remote": m.dte.RemoteAddr().String(), + }).Debug("Shutting down DCE") + m.disconnect() + m.noCarrier() + }() + + // suppress GA (3) + // binary transmission (0) + // echo (1) + m.tnWrite([]TCode{ + TN_IAC, TN_WILL, TCode(3), + TN_IAC, TN_DO, TCode(3), + TN_IAC, TN_WILL, TCode(0), + TN_IAC, TN_DO, TCode(0), + TN_IAC, TN_WONT, TCode(1), + TN_IAC, TN_DO, TCode(1), + }) + + m.state = TN_NORM + + for { + + // wait if necessary until we can read + now := time.Now() + if m.readSlot.After(now) { + rtime := m.readSlot.Sub(now) + fmt.Printf("waiting %d for read slot\n", rtime) + time.Sleep(rtime) + } + + // read some data + available, err := m.dce.Read(m.dceBuff) + + // set the next available timeslot + tlen := time.Duration((9 * 1000 * 1000 * 1000 * len(m.dceBuff)) / int(m.baud)) + fmt.Printf("%d chars, next read slot in %d\n", len(m.dceBuff), tlen) + m.readSlot = now.Add(tlen) + + if err != nil { + if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { + log.WithFields(log.Fields{ + "remote": m.dte.RemoteAddr().String(), + }).Info("Telnet DCE closed") + return + } else { + log.WithFields(log.Fields{ + "remote": m.dte.RemoteAddr().String(), + "error": err, + }).Error("DCE Read error") + return + } + } + + // parse the read data for control chars + for scan := 0; scan < available; { + switch m.state { + case TN_NORM: + // normal data processing, scan for IAC code + + start := scan + for ; scan < available; scan++ { + if TCode(m.dceBuff[scan]) == TN_IAC { + m.state = TN_IAC + break + } + } + + // write data so far + data := m.dceBuff[start:scan] + m.dte.Write(data) + + if scan < available { + // TN_IAC was received, skip over it + scan++ + } + + case TN_IAC: + // have received IAC look for the next code + code := TCode(m.dceBuff[scan]) + scan++ + + switch code { + case TN_IAC: + // actually send 255 + m.dte.Write([]byte{byte(TN_IAC)}) + case TN_WILL: + m.state = TN_WILL + case TN_WONT: + m.state = TN_WONT + case TN_DO: + m.state = TN_DO + case TN_DONT: + m.state = TN_DO + default: + log.WithFields(log.Fields{ + "code": code, + }).Debug("Un-implemented TN code") + m.state = TN_NORM + } + + case TN_WILL: + + code := TCode(m.dceBuff[scan]) + m.state = TN_NORM + scan++ + + switch code { + case 0: + // ignore binary confirmation + case 1: + // ignore echo + case 3: + // ignore GA confirmation + default: + // refuse any other WILL codes + m.tnWrite([]TCode{TN_IAC, TN_DONT, code}) + log.WithFields(log.Fields{ + "code": code, + }).Debug("Un-implemented TN_WILL") + } + + case TN_WONT: + + code := TCode(m.dceBuff[scan]) + m.state = TN_NORM + scan++ + + switch code { + default: + log.WithFields(log.Fields{ + "code": code, + }).Debug("Un-implemented TN_WONT") + } + + case TN_DO: + + code := TCode(m.dceBuff[scan]) + m.state = TN_NORM + scan++ + + switch code { + case 0: + // ignore binary confirmation + case 3: + // ignore GA confirmation + default: + // refuse any DO codes + m.tnWrite([]TCode{TN_IAC, TN_WONT, code}) + log.WithFields(log.Fields{ + "code": code, + }).Debug("Un-implemented TN_DO") + } + + case TN_DONT: + + code := TCode(m.dceBuff[scan]) + m.state = TN_NORM + scan++ + + switch code { + default: + // confirm won't do any codes + m.tnWrite([]TCode{TN_IAC, TN_WONT, code}) + log.WithFields(log.Fields{ + "code": code, + }).Debug("Un-implemented TN_DONT") + } + + default: + log.WithFields(log.Fields{ + "state": m.state, + }).Debug("Unknown TN state") + } + + } + } + +} + +////////////////////////////////////////////////////////////////////////// +// everything starts here + +func main() { + + log.SetLevel(log.DebugLevel) + log.Info("modemulator starting") + + // startup + Modems = make(map[string]*Modem) + Listen() + + // graceful shutdown via SIGINT (^C) + csig := make(chan os.Signal, 1) + signal.Notify(csig, os.Interrupt) + + // and block + <-csig + + log.Info("Server shutting down") + + // shutdown listeners here + for _, listener := range Listeners { + listener.Close() + } + + // shutdown active modems + for _, modem := range Modems { + modem.Shutdown() + } + + // nothing left to do + log.Info("Shutdown complete, all done") + os.Exit(0) +} + +////////////////////////////////////////////////////////////////////////// +// end of code