// Package portservice exposes per-channel SSH and Telnet operations without
// granting network-facing processes direct access to raw UART device paths.
package portservice
import (
"bufio"
"bytes"
"context"
"crypto/subtle"
"errors"
"fmt"
"io"
"net"
"reflect"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/knownhosts"
"tera-clone/services/tera-api/internal/directstream"
"tera-clone/services/tera-api/internal/domain"
)
type AuthenticateFunc func ( context . Context , string , string , string ) ( string , error )
type AuthenticateMethodFunc func ( context . Context , string , string , string , string ) ( string , error )
type RecordFunc func ( context . Context , domain . PortLogEvent ) error
type ListenFunc func ( string , string ) ( net . Listener , error )
type DialFunc func ( context . Context , string , string ) ( net . Conn , error )
type Options struct {
TransportFactory directstream . TransportFactory
Authenticate AuthenticateFunc
AuthenticateMethod AuthenticateMethodFunc
Record RecordFunc
Listen ListenFunc
Dial DialFunc
ListenHost string
ListenInterface func ( string , string ) ( net . Listener , error )
SSHSigner ssh . Signer
KnownHostsPath string
RetryDelay time . Duration
}
type Controller interface {
Apply ( context . Context , [ ] domain . Port ) error
}
type SnapshotProvider interface {
Snapshot ( ) Snapshot
}
type NoopController struct { }
func ( NoopController ) Apply ( _ context . Context , ports [ ] domain . Port ) error {
_ , err := operationConfigs ( ports )
return err
}
type Gateway struct {
options Options
broker * directstream . Broker
mu sync . Mutex
services map [ string ] * service
sequence atomic . Uint64
pools map [ string ] * sessionPool
}
type serviceConfig struct {
key string
channel string
number int
operation domain . PortOperation
sources [ ] string
targets [ ] string
}
type service struct {
config serviceConfig
listener net . Listener
cancel context . CancelFunc
done chan struct { }
clients atomic . Int32
pending atomic . Int32
pool * sessionPool
}
type Snapshot struct {
Services [ ] ServiceSnapshot ` json:"services" `
}
type ServiceSnapshot struct {
Channel string ` json:"channel" `
Operation int ` json:"operation" `
Mode string ` json:"mode" `
Port int ` json:"port" `
Host string ` json:"host,omitempty" `
State string ` json:"state" `
ActiveClients int ` json:"active_clients" `
SessionIPs [ ] string ` json:"session_ips" `
Interface string ` json:"interface" `
Sources [ ] string ` json:"sources,omitempty" `
Targets [ ] string ` json:"targets,omitempty" `
}
func New ( options Options ) ( * Gateway , error ) {
if options . TransportFactory == nil {
return nil , errors . New ( "port service transport factory is required" )
}
if options . Listen == nil {
options . Listen = net . Listen
}
if options . Dial == nil {
dialer := & net . Dialer { Timeout : 10 * time . Second , KeepAlive : 30 * time . Second }
options . Dial = dialer . DialContext
}
if strings . TrimSpace ( options . ListenHost ) == "" {
options . ListenHost = "0.0.0.0"
}
if options . RetryDelay <= 0 {
options . RetryDelay = 5 * time . Second
}
broker , err := directstream . NewBroker ( options . TransportFactory , directstream . BrokerOptions { MaxAttachmentsPerChannel : 16 } )
if err != nil {
return nil , err
}
return & Gateway { options : options , broker : broker , services : make ( map [ string ] * service ) , pools : make ( map [ string ] * sessionPool ) } , nil
}
// Apply reconciles the active operation set. Unchanged services are retained so
// configuration changes on one channel do not interrupt sessions on another.
// Callers should pass the running configuration only after the repository
// transaction has selected it.
func ( g * Gateway ) Apply ( ctx context . Context , ports [ ] domain . Port ) error {
desired , err := operationConfigs ( ports )
if err != nil {
return err
}
g . mu . Lock ( )
defer g . mu . Unlock ( )
if err := ctx . Err ( ) ; err != nil {
return err
}
desiredByKey := make ( map [ string ] serviceConfig , len ( desired ) )
for _ , config := range desired {
desiredByKey [ config . key ] = config
}
oldConfigs := make ( [ ] serviceConfig , 0 , len ( g . services ) )
for _ , current := range g . services {
oldConfigs = append ( oldConfigs , current . config )
}
sort . Slice ( oldConfigs , func ( left , right int ) bool { return oldConfigs [ left ] . key < oldConfigs [ right ] . key } )
retained := make ( map [ string ] * service , len ( g . services ) )
stoppedConfigs := make ( [ ] serviceConfig , 0 , len ( oldConfigs ) )
for _ , previous := range oldConfigs {
current := g . services [ previous . key ]
next , exists := desiredByKey [ previous . key ]
if exists && reflect . DeepEqual ( previous , next ) {
retained [ previous . key ] = current
continue
}
current . stop ( )
stoppedConfigs = append ( stoppedConfigs , previous )
}
toStart := make ( [ ] serviceConfig , 0 , len ( desired ) - len ( retained ) )
for _ , config := range desired {
if _ , exists := retained [ config . key ] ; ! exists {
toStart = append ( toStart , config )
}
}
if len ( stoppedConfigs ) == 0 && len ( toStart ) == 0 {
return nil
}
nextServices := make ( map [ string ] * service , len ( desired ) )
for key , current := range retained {
nextServices [ key ] = current
}
started := make ( [ ] * service , 0 , len ( toStart ) )
for _ , config := range toStart {
current , startErr := g . startLocked ( config )
if startErr != nil {
for _ , item := range started {
item . stop ( )
}
restoredServices := make ( map [ string ] * service , len ( oldConfigs ) )
for key , item := range retained {
restoredServices [ key ] = item
}
var rollbackErrors [ ] error
for _ , previous := range stoppedConfigs {
restored , restoreErr := g . startLocked ( previous )
if restoreErr != nil {
rollbackErrors = append ( rollbackErrors , fmt . Errorf ( "restore %s: %w" , previous . key , restoreErr ) )
continue
}
restoredServices [ previous . key ] = restored
}
g . services = restoredServices
applyErr := fmt . Errorf ( "start %s: %w" , config . key , startErr )
if len ( rollbackErrors ) > 0 {
return errors . Join ( append ( [ ] error { applyErr } , rollbackErrors ... ) ... )
}
return applyErr
}
nextServices [ config . key ] = current
started = append ( started , current )
}
g . services = nextServices
return nil
}
func ( g * Gateway ) Snapshot ( ) Snapshot {
g . mu . Lock ( )
defer g . mu . Unlock ( )
result := Snapshot { Services : make ( [ ] ServiceSnapshot , 0 , len ( g . services ) ) }
for _ , item := range g . services {
result . Services = append ( result . Services , ServiceSnapshot {
Channel : item . config . channel , Operation : item . config . number , Mode : item . config . operation . Mode ,
Port : item . config . operation . Port , Host : item . config . operation . Host , State : serviceState ( item . config ) ,
ActiveClients : int ( item . clients . Load ( ) ) ,
SessionIPs : item . pool . snapshot ( ) , Interface : fmt . Sprintf ( "eth%d" , item . config . number - 1 ) ,
Sources : append ( [ ] string ( nil ) , item . config . sources ... ) ,
Targets : append ( [ ] string ( nil ) , item . config . targets ... ) ,
} )
}
sort . Slice ( result . Services , func ( left , right int ) bool {
if result . Services [ left ] . Channel != result . Services [ right ] . Channel {
return result . Services [ left ] . Channel < result . Services [ right ] . Channel
}
return result . Services [ left ] . Operation < result . Services [ right ] . Operation
} )
return result
}
func ( g * Gateway ) Close ( ) error {
g . mu . Lock ( )
for _ , current := range g . services {
current . stop ( )
}
g . services = make ( map [ string ] * service )
g . mu . Unlock ( )
return g . broker . Close ( )
}
func ( s * service ) stop ( ) {
if s . cancel != nil {
s . cancel ( )
}
if s . listener != nil {
_ = s . listener . Close ( )
}
if s . done != nil {
select {
case <- s . done :
case <- time . After ( 2 * time . Second ) :
}
}
}
func operationConfigs ( ports [ ] domain . Port ) ( [ ] serviceConfig , error ) {
result := make ( [ ] serviceConfig , 0 , len ( ports ) * 2 )
listeners := make ( map [ string ] string )
redirects := make ( map [ string ] [ ] string )
for _ , port := range ports {
if directstream . ValidateChannel ( port . Channel ) != nil {
return nil , fmt . Errorf ( "invalid operation channel %q" , port . Channel )
}
for index , operation := range [ ] domain . PortOperation { port . Operation1 , port . Operation2 } {
if operation . Mode == "" || operation . Mode == "disabled" {
continue
}
operation . MaxClients = port . Operation1 . MaxClients
config := serviceConfig { key : fmt . Sprintf ( "%s-operation-%d" , port . Channel , index + 1 ) , channel : port . Channel , number : index + 1 , operation : operation }
switch operation . Mode {
case "reverse_telnet" , "reverse_ssh" :
if operation . Port < 1 || operation . Port > 65535 {
return nil , fmt . Errorf ( "%s listener port is invalid" , config . key )
}
listenerKey := fmt . Sprintf ( "%d:%d" , index , operation . Port )
if previous := listeners [ listenerKey ] ; previous != "" {
return nil , fmt . Errorf ( "listener port %d conflicts between %s and %s" , operation . Port , previous , config . key )
}
listeners [ listenerKey ] = config . key
case "telnet" , "ssh" :
if strings . TrimSpace ( operation . Host ) == "" || operation . Port < 1 || operation . Port > 65535 {
return nil , fmt . Errorf ( "%s normal mode requires host and port" , config . key )
}
default :
return nil , fmt . Errorf ( "%s mode is unsupported" , config . key )
}
result = append ( result , config )
}
if len ( port . RedirectTo ) > 0 {
targets := append ( [ ] string ( nil ) , port . RedirectTo ... )
sort . Strings ( targets )
for _ , target := range targets {
if directstream . ValidateChannel ( target ) != nil || target == port . Channel {
return nil , fmt . Errorf ( "%s redirect target %q is invalid" , port . Channel , target )
}
}
redirects [ port . Channel ] = targets
}
}
if err := validateRedirectGraph ( redirects ) ; err != nil {
return nil , err
}
sourcesByTarget := make ( map [ string ] [ ] string )
for source , targets := range redirects {
for _ , target := range targets {
sourcesByTarget [ target ] = append ( sourcesByTarget [ target ] , source )
}
}
for target , sources := range sourcesByTarget {
sort . Strings ( sources )
result = append ( result , serviceConfig {
key : "redirect-to-" + target , channel : target , operation : domain . PortOperation { Mode : "redirect" } ,
sources : sources , targets : [ ] string { target } ,
} )
}
sort . Slice ( result , func ( left , right int ) bool { return result [ left ] . key < result [ right ] . key } )
return result , nil
}
func validateRedirectGraph ( graph map [ string ] [ ] string ) error {
state := make ( map [ string ] uint8 , len ( graph ) )
var visit func ( string ) error
visit = func ( channel string ) error {
switch state [ channel ] {
case 1 :
return fmt . Errorf ( "redirect graph contains a cycle at %s" , channel )
case 2 :
return nil
}
state [ channel ] = 1
for _ , target := range graph [ channel ] {
if _ , redirects := graph [ target ] ; redirects {
if err := visit ( target ) ; err != nil {
return err
}
}
}
state [ channel ] = 2
return nil
}
for channel := range graph {
if err := visit ( channel ) ; err != nil {
return err
}
}
return nil
}
func serviceState ( config serviceConfig ) string {
switch config . operation . Mode {
case "reverse_ssh" , "reverse_telnet" :
return "listening"
case "redirect" :
return "forwarding"
default :
return "reconnecting"
}
}
func ( g * Gateway ) startLocked ( config serviceConfig ) ( * service , error ) {
ctx , cancel := context . WithCancel ( context . Background ( ) )
pool := g . pools [ config . channel ]
if pool == nil {
pool = & sessionPool { }
g . pools [ config . channel ] = pool
}
current := & service { config : config , cancel : cancel , done : make ( chan struct { } ) , pool : pool }
switch config . operation . Mode {
case "reverse_telnet" , "reverse_ssh" :
if config . operation . Mode == "reverse_ssh" && g . options . SSHSigner == nil {
cancel ( )
return nil , errors . New ( "reverse SSH host key is unavailable" )
}
if err := validateOperationCredential ( config ) ; err != nil {
cancel ( )
return nil , err
}
address := net . JoinHostPort ( g . options . ListenHost , strconv . Itoa ( config . operation . Port ) )
var listener net . Listener
var err error
if g . options . ListenInterface != nil {
listener , err = g . options . ListenInterface ( fmt . Sprintf ( "eth%d" , config . number - 1 ) , address )
} else {
listener , err = g . options . Listen ( "tcp" , address )
}
if err != nil {
cancel ( )
return nil , err
}
current . listener = listener
go g . runListener ( ctx , current )
case "telnet" , "ssh" :
if config . operation . Mode == "ssh" && strings . TrimSpace ( g . options . KnownHostsPath ) == "" {
cancel ( )
return nil , errors . New ( "normal SSH requires a known_hosts path" )
}
if err := validateOperationCredential ( config ) ; err != nil {
cancel ( )
return nil , err
}
go g . runNormal ( ctx , current )
case "redirect" :
go g . runRedirect ( ctx , current )
}
return current , nil
}
func validateOperationCredential ( config serviceConfig ) error {
operation := config . operation
if operation . Authentication == "password" && operation . Credential == "" {
return errors . New ( "password authentication credential is unavailable" )
}
if operation . Authentication != "key" {
return nil
}
if operation . Credential == "" {
return errors . New ( "key authentication credential is unavailable" )
}
if operation . Mode == "reverse_ssh" {
if _ , _ , _ , _ , err := ssh . ParseAuthorizedKey ( [ ] byte ( operation . Credential ) ) ; err != nil {
return fmt . Errorf ( "parse authorized SSH key: %w" , err )
}
return nil
}
if operation . Mode == "ssh" {
if _ , err := ssh . ParsePrivateKey ( [ ] byte ( operation . Credential ) ) ; err != nil {
return fmt . Errorf ( "parse SSH private key: %w" , err )
}
return nil
}
return errors . New ( "key authentication is only valid for SSH" )
}
func ( g * Gateway ) runListener ( ctx context . Context , current * service ) {
defer close ( current . done )
for {
connection , err := current . listener . Accept ( )
if err != nil {
if ctx . Err ( ) != nil || errors . Is ( err , net . ErrClosed ) {
return
}
continue
}
// Authentication handshakes are not active terminal sessions and must not
// consume MaxClients. Keep a separate bounded pre-authentication ceiling
// so incomplete handshakes still cannot exhaust the process.
if current . pending . Add ( 1 ) > 16 {
current . pending . Add ( - 1 )
_ , _ = connection . Write ( [ ] byte ( "Too many pending connections.\r\n" ) )
_ = connection . Close ( )
continue
}
go func ( ) {
defer current . pending . Add ( - 1 )
defer connection . Close ( )
if current . config . operation . Mode == "reverse_ssh" {
g . serveSSH ( ctx , connection , current )
} else {
g . serveTelnet ( ctx , connection , current )
}
} ( )
}
}
func ( g * Gateway ) serveTelnet ( ctx context . Context , connection net . Conn , current * service ) {
config := current . config
actor := "anonymous"
reader := bufio . NewReader ( & telnetReader { reader : connection } )
if isManagedAuthentication ( config . operation . Authentication ) {
if g . options . Authenticate == nil && g . options . AuthenticateMethod == nil {
return
}
_ , _ = connection . Write ( [ ] byte ( "TERA CORA-X48 PRO K " + strings . ToUpper ( config . operation . Authentication ) + " authentication\r\nUsername: " ) )
username , err := readCredentialLine ( reader , 64 )
if err != nil {
return
}
_ , _ = connection . Write ( [ ] byte ( "Password: " ) )
password , err := readCredentialLine ( reader , 256 )
_ , _ = connection . Write ( [ ] byte ( "\r\n" ) )
if err != nil {
return
}
actor , err = g . authenticate ( ctx , config . operation . Authentication , username , password , config . channel )
if err != nil {
_ , _ = connection . Write ( [ ] byte ( "Authentication failed.\r\n" ) )
return
}
} else if config . operation . Authentication == "password" {
_ , _ = connection . Write ( [ ] byte ( "TERA CORA-X48 PRO K password authentication\r\nUsername: " ) )
username , err := readCredentialLine ( reader , 64 )
if err != nil {
return
}
_ , _ = connection . Write ( [ ] byte ( "Password: " ) )
password , err := readCredentialLine ( reader , 256 )
_ , _ = connection . Write ( [ ] byte ( "\r\n" ) )
if err != nil || ! credentialMatches ( config . operation . Credential , password ) || ( config . operation . Username != "" && config . operation . Username != username ) {
_ , _ = connection . Write ( [ ] byte ( "Authentication failed.\r\n" ) )
return
}
actor = username
} else if config . operation . Authentication != "none" {
return
}
release , accepted := beginTerminalSession ( current , connection . RemoteAddr ( ) . String ( ) )
if ! accepted {
_ , _ = connection . Write ( [ ] byte ( "Too many clients.\r\n" ) )
return
}
defer release ( )
g . bridge ( ctx , connection , reader , config , actor , remoteEndpoint ( connection . RemoteAddr ( ) ) , true )
}
func ( g * Gateway ) serveSSH ( ctx context . Context , connection net . Conn , current * service ) {
config := current . config
serverConfig := & ssh . ServerConfig { ServerVersion : "SSH-2.0-TERA-CORA-X48-PRO-K" }
serverConfig . AddHostKey ( g . options . SSHSigner )
if config . operation . Authentication == "none" {
serverConfig . NoClientAuth = true
} else if isManagedAuthentication ( config . operation . Authentication ) && ( g . options . Authenticate != nil || g . options . AuthenticateMethod != nil ) {
serverConfig . PasswordCallback = func ( metadata ssh . ConnMetadata , password [ ] byte ) ( * ssh . Permissions , error ) {
actor , err := g . authenticate ( ctx , config . operation . Authentication , metadata . User ( ) , string ( password ) , config . channel )
if err != nil {
return nil , err
}
return & ssh . Permissions { Extensions : map [ string ] string { "actor" : actor } } , nil
}
} else if config . operation . Authentication == "password" {
serverConfig . PasswordCallback = func ( metadata ssh . ConnMetadata , password [ ] byte ) ( * ssh . Permissions , error ) {
if ( config . operation . Username != "" && config . operation . Username != metadata . User ( ) ) || ! credentialMatches ( config . operation . Credential , string ( password ) ) {
return nil , errors . New ( "authentication failed" )
}
return & ssh . Permissions { Extensions : map [ string ] string { "actor" : metadata . User ( ) } } , nil
}
} else if config . operation . Authentication == "key" {
authorized , _ , _ , _ , err := ssh . ParseAuthorizedKey ( [ ] byte ( config . operation . Credential ) )
if err != nil {
return
}
serverConfig . PublicKeyCallback = func ( metadata ssh . ConnMetadata , key ssh . PublicKey ) ( * ssh . Permissions , error ) {
if ( config . operation . Username != "" && config . operation . Username != metadata . User ( ) ) || ! bytes . Equal ( authorized . Marshal ( ) , key . Marshal ( ) ) {
return nil , errors . New ( "authentication failed" )
}
return & ssh . Permissions { Extensions : map [ string ] string { "actor" : metadata . User ( ) } } , nil
}
} else {
return
}
serverConnection , channels , requests , err := ssh . NewServerConn ( connection , serverConfig )
if err != nil {
return
}
defer serverConnection . Close ( )
go ssh . DiscardRequests ( requests )
for channelRequest := range channels {
if channelRequest . ChannelType ( ) != "session" {
_ = channelRequest . Reject ( ssh . UnknownChannelType , "session channel required" )
continue
}
channel , channelRequests , err := channelRequest . Accept ( )
if err != nil {
continue
}
go acceptShellRequests ( channelRequests )
release , accepted := beginTerminalSession ( current , serverConnection . RemoteAddr ( ) . String ( ) )
if ! accepted {
_ , _ = channel . Write ( [ ] byte ( "Too many clients.\r\n" ) )
_ = channel . Close ( )
return
}
actor := serverConnection . User ( )
if serverConnection . Permissions != nil && serverConnection . Permissions . Extensions [ "actor" ] != "" {
actor = serverConnection . Permissions . Extensions [ "actor" ]
}
g . bridge ( ctx , channel , channel , config , actor , remoteEndpoint ( serverConnection . RemoteAddr ( ) ) , true )
release ( )
_ = channel . Close ( )
return
}
}
type sessionPool struct {
mu sync . Mutex
slots [ 4 ] string
}
func ( p * sessionPool ) snapshot ( ) [ ] string {
if p == nil {
return [ ] string { "" , "" , "" , "" }
}
p . mu . Lock ( )
defer p . mu . Unlock ( )
return append ( [ ] string ( nil ) , p . slots [ : ] ... )
}
func beginTerminalSession ( current * service , endpoint string ) ( func ( ) , bool ) {
maximum := current . config . operation . MaxClients
if maximum < 1 {
maximum = 1
}
if maximum > 4 {
maximum = 4
}
host , _ , err := net . SplitHostPort ( endpoint )
if err != nil {
host = endpoint
}
if host == "" {
host = "unknown"
}
pool := current . pool
pool . mu . Lock ( )
defer pool . mu . Unlock ( )
active := 0
for _ , value := range pool . slots {
if value != "" {
active ++
}
}
if active >= maximum {
return nil , false
}
for index := range pool . slots {
if pool . slots [ index ] != "" {
continue
}
pool . slots [ index ] = host
current . clients . Add ( 1 )
var once sync . Once
return func ( ) {
once . Do ( func ( ) { pool . mu . Lock ( ) ; pool . slots [ index ] = "" ; pool . mu . Unlock ( ) ; current . clients . Add ( - 1 ) } )
} , true
}
return nil , false
}
func acceptShellRequests ( requests <- chan * ssh . Request ) {
for request := range requests {
switch request . Type {
case "pty-req" , "env" , "window-change" , "shell" :
if request . WantReply {
_ = request . Reply ( true , nil )
}
default :
if request . WantReply {
_ = request . Reply ( false , nil )
}
}
}
}
func ( g * Gateway ) runNormal ( ctx context . Context , current * service ) {
defer close ( current . done )
for {
if ctx . Err ( ) != nil {
return
}
if current . config . operation . Mode == "ssh" {
g . connectNormalSSH ( ctx , current . config )
} else {
g . connectNormalTelnet ( ctx , current . config )
}
timer := time . NewTimer ( g . options . RetryDelay )
select {
case <- ctx . Done ( ) :
timer . Stop ( )
return
case <- timer . C :
}
}
}
func ( g * Gateway ) connectNormalTelnet ( ctx context . Context , config serviceConfig ) {
connection , err := g . options . Dial ( ctx , "tcp" , net . JoinHostPort ( config . operation . Host , strconv . Itoa ( config . operation . Port ) ) )
if err != nil {
g . recordFailure ( config , err )
return
}
defer connection . Close ( )
if config . operation . Username != "" {
_ , _ = connection . Write ( append ( [ ] byte ( config . operation . Username ) , operationNewline ( config . operation . Newline ) ... ) )
}
if config . operation . Credential != "" {
_ , _ = connection . Write ( append ( [ ] byte ( config . operation . Credential ) , operationNewline ( config . operation . Newline ) ... ) )
}
g . bridge ( ctx , connection , & telnetReader { reader : connection } , config , config . operation . Username , remoteEndpoint ( connection . RemoteAddr ( ) ) , false )
}
func ( g * Gateway ) connectNormalSSH ( ctx context . Context , config serviceConfig ) {
hostKeyCallback , err := knownhosts . New ( g . options . KnownHostsPath )
if err != nil {
g . recordFailure ( config , err )
return
}
clientConfig := & ssh . ClientConfig { User : config . operation . Username , HostKeyCallback : hostKeyCallback , Timeout : operationTimeout ( config . operation ) }
switch config . operation . Authentication {
case "password" , "local" :
if config . operation . Credential != "" {
clientConfig . Auth = [ ] ssh . AuthMethod { ssh . Password ( config . operation . Credential ) }
}
case "key" :
signer , parseErr := ssh . ParsePrivateKey ( [ ] byte ( config . operation . Credential ) )
if parseErr != nil {
g . recordFailure ( config , parseErr )
return
}
clientConfig . Auth = [ ] ssh . AuthMethod { ssh . PublicKeys ( signer ) }
}
address := net . JoinHostPort ( config . operation . Host , strconv . Itoa ( config . operation . Port ) )
connection , err := g . options . Dial ( ctx , "tcp" , address )
if err != nil {
g . recordFailure ( config , err )
return
}
defer connection . Close ( )
clientConnection , channels , requests , err := ssh . NewClientConn ( connection , address , clientConfig )
if err != nil {
g . recordFailure ( config , err )
return
}
client := ssh . NewClient ( clientConnection , channels , requests )
defer client . Close ( )
channel , channelRequests , err := client . OpenChannel ( "session" , nil )
if err != nil {
g . recordFailure ( config , err )
return
}
defer channel . Close ( )
go ssh . DiscardRequests ( channelRequests )
_ , err = channel . SendRequest ( "shell" , true , nil )
if err != nil {
g . recordFailure ( config , err )
return
}
g . bridge ( ctx , channel , channel , config , config . operation . Username , remoteEndpoint ( connection . RemoteAddr ( ) ) , false )
}
func ( g * Gateway ) runRedirect ( ctx context . Context , current * service ) {
defer close ( current . done )
for {
if ctx . Err ( ) != nil {
return
}
if err := g . forwardRedirect ( ctx , current . config ) ; err != nil && ctx . Err ( ) == nil {
g . recordFailure ( current . config , err )
}
timer := time . NewTimer ( g . options . RetryDelay )
select {
case <- ctx . Done ( ) :
timer . Stop ( )
return
case <- timer . C :
}
}
}
func ( g * Gateway ) forwardRedirect ( ctx context . Context , config serviceConfig ) error {
sequence := g . sequence . Add ( 1 )
targetChannel := config . channel
targetSession := fmt . Sprintf ( "redirect-to-%s-%d" , strings . ToLower ( targetChannel ) , sequence )
target , err := g . broker . Attach ( ctx , directstream . AttachRequest {
SessionID : targetSession , Channel : targetChannel , Actor : "tera-redirect" , Access : directstream . AccessWriter ,
} )
if err != nil {
return fmt . Errorf ( "attach target %s: %w" , targetChannel , err )
}
defer target . Close ( )
type sourceAttachment struct {
channel string
attachment * directstream . Attachment
}
sources := make ( [ ] sourceAttachment , 0 , len ( config . sources ) )
defer func ( ) {
for _ , source := range sources {
_ = source . attachment . Close ( )
}
} ( )
for _ , channel := range config . sources {
source , attachErr := g . broker . Attach ( ctx , directstream . AttachRequest {
SessionID : fmt . Sprintf ( "redirect-%s-to-%s-%d" , strings . ToLower ( channel ) , strings . ToLower ( targetChannel ) , sequence ) ,
Channel : channel , Actor : "tera-redirect" , Access : directstream . AccessViewer ,
} )
if attachErr != nil {
return fmt . Errorf ( "attach source %s: %w" , channel , attachErr )
}
sources = append ( sources , sourceAttachment { channel : channel , attachment : source } )
}
connectedAt := time . Now ( ) . UTC ( )
message := fmt . Sprintf ( "TERA CORA-X48 PRO K serial redirect connected\nSource channels: %s\nTarget channel: %s\nSession: %s\nConnected at: %s" , strings . Join ( config . sources , "," ) , targetChannel , targetSession , connectedAt . Format ( time . RFC3339 ) )
_ = g . recordSession ( config , "tera-redirect" , endpoint { host : "local" } , targetSession , "connected" , message , connectedAt )
errorsChannel := make ( chan error , len ( sources ) )
for _ , item := range sources {
go func ( source sourceAttachment ) {
for {
payload , receiveErr := source . attachment . Receive ( ctx )
if receiveErr != nil {
errorsChannel <- receiveErr
return
}
if writeErr := target . Write ( ctx , payload ) ; writeErr != nil {
errorsChannel <- fmt . Errorf ( "write %s to %s: %w" , source . channel , targetChannel , writeErr )
return
}
}
} ( item )
}
select {
case <- ctx . Done ( ) :
return ctx . Err ( )
case forwardErr := <- errorsChannel :
return forwardErr
}
}
type readWriteCloser interface {
io . Reader
io . Writer
io . Closer
}
func ( g * Gateway ) bridge ( ctx context . Context , connection readWriteCloser , reader io . Reader , config serviceConfig , actor string , remote endpoint , welcome bool ) {
if strings . TrimSpace ( actor ) == "" {
actor = "anonymous"
}
sessionID := fmt . Sprintf ( "operation-%s-%d-%d" , strings . ToLower ( config . channel ) , config . number , g . sequence . Add ( 1 ) )
attachment , err := g . broker . Attach ( ctx , directstream . AttachRequest {
SessionID : sessionID , Channel : config . channel , Actor : actor ,
Access : directstream . AccessWriter , SharedWrite : true ,
} )
if err != nil {
g . recordFailure ( config , err )
return
}
defer attachment . Close ( )
connectedAt := time . Now ( ) . UTC ( )
message := welcomeMessage ( config , actor , remote , sessionID , connectedAt )
_ = g . recordSession ( config , actor , remote , sessionID , "connected" , message , connectedAt )
if welcome {
_ , _ = connection . Write ( [ ] byte ( "\r\n" + strings . ReplaceAll ( message , "\n" , "\r\n" ) + "\r\n\r\n" ) )
}
streamContext , cancel := context . WithCancel ( ctx )
defer cancel ( )
shortcuts := terminalShortcutSet ( config . operation )
var connectionWriteMu sync . Mutex
writeConnection := func ( payload [ ] byte ) error {
connectionWriteMu . Lock ( )
defer connectionWriteMu . Unlock ( )
_ , err := connection . Write ( payload )
return err
}
writeNotice := func ( message string ) error {
return writeConnection ( [ ] byte ( "\r\n[TERA] " + message + "\r\n" ) )
}
done := make ( chan struct { } , 2 )
go func ( ) {
defer func ( ) { done <- struct { } { } } ( )
buffer := make ( [ ] byte , 32 * 1024 )
controlMode := false
for {
count , readErr := reader . Read ( buffer )
if count > 0 {
payload := make ( [ ] byte , 0 , count )
for _ , value := range buffer [ : count ] {
if controlMode {
switch value {
case '?' :
if writeNotice ( controlModeHelp ( shortcuts ) ) != nil {
return
}
continue
case 'q' , 'Q' :
_ = writeNotice ( "current session closed" )
return
case 't' , 'T' :
controlMode = false
if writeNotice ( config . channel + " terminal resumed; all other Telnet sessions remain connected" ) != nil {
return
}
continue
case 'b' , 'B' :
controlMode = false
if err := attachment . SendBreak ( streamContext ) ; err != nil {
if writeNotice ( "BREAK failed: " + err . Error ( ) ) != nil {
return
}
} else if writeNotice ( "BREAK sent to " + config . channel ) != nil {
return
}
continue
case 'i' , 'I' :
_ = writeNotice ( "session interrupted" )
return
default :
controlMode = false
if writeNotice ( "unknown control command; terminal resumed" ) != nil {
return
}
continue
}
}
switch {
case shortcuts . control . matches ( value ) :
controlMode = true
if writeNotice ( controlModeHelp ( shortcuts ) ) != nil {
return
}
case shortcuts . switchSession . matches ( value ) :
if writeNotice ( config . channel + " terminal active; all connected terminal sessions remain online" ) != nil {
return
}
case shortcuts . quit . matches ( value ) :
_ = writeNotice ( "current session closed" )
return
case shortcuts . sendBreak . matches ( value ) :
if err := attachment . SendBreak ( streamContext ) ; err != nil {
if writeNotice ( "BREAK failed: " + err . Error ( ) ) != nil {
return
}
} else if writeNotice ( "BREAK sent to " + config . channel ) != nil {
return
}
case shortcuts . interrupt . matches ( value ) :
_ = writeNotice ( "session interrupted" )
return
default :
payload = append ( payload , value )
}
}
if len ( payload ) > 0 {
payload = normalizeNewlines ( payload , config . operation . Newline )
if writeErr := attachment . Write ( streamContext , payload ) ; writeErr != nil {
return
}
}
}
if readErr != nil {
return
}
}
} ( )
go func ( ) {
defer func ( ) { done <- struct { } { } } ( )
for {
payload , receiveErr := attachment . Receive ( streamContext )
if receiveErr != nil {
return
}
if writeErr := writeConnection ( payload ) ; writeErr != nil {
return
}
}
} ( )
<- done
cancel ( )
_ = connection . Close ( )
<- done
disconnectedAt := time . Now ( ) . UTC ( )
disconnect := fmt . Sprintf ( "TERA CORA-X48 PRO K session disconnected\nConnected from: %s\nSerial channel: %s\nProtocol: %s\nUser: %s\nSession: %s\nDisconnected at: %s" , remote . host , config . channel , config . operation . Mode , actor , sessionID , disconnectedAt . Format ( time . RFC3339 ) )
_ = g . recordSession ( config , actor , remote , sessionID , "disconnected" , disconnect , disconnectedAt )
}
func ( g * Gateway ) recordSession ( config serviceConfig , actor string , remote endpoint , sessionID , eventType , message string , timestamp time . Time ) error {
if g . options . Record == nil {
return nil
}
return g . options . Record ( context . Background ( ) , domain . PortLogEvent {
Timestamp : timestamp , Channel : config . channel , EventType : "session" ,
Actor : actor + "/" + sessionID , Message : eventType + ": " + message , RemoteAddress : remote . host , RemotePort : remote . port ,
ShowTimestamp : true , ShowUsername : true , ShowIPAddress : true , ShowPort : true ,
} )
}
func ( g * Gateway ) recordFailure ( config serviceConfig , err error ) {
if g . options . Record == nil || err == nil {
return
}
_ , port := remoteEndpointString ( config . operation . Host , config . operation . Port )
_ = g . options . Record ( context . Background ( ) , domain . PortLogEvent {
Timestamp : time . Now ( ) . UTC ( ) , Channel : config . channel , EventType : "command" ,
Actor : "tera-api" , Command : config . key , ReturnValue : err . Error ( ) , Message : config . key + ": " + err . Error ( ) , RemoteAddress : config . operation . Host , RemotePort : port ,
ShowTimestamp : true , ShowIPAddress : true , ShowPort : true ,
} )
}
func welcomeMessage ( config serviceConfig , actor string , remote endpoint , sessionID string , timestamp time . Time ) string {
shortcuts := terminalShortcutSet ( config . operation )
return fmt . Sprintf ( "Welcome to TERA CORA-X48 PRO K\nConnected from: %s:%d\nSerial channel: %s\nOperation: %d\nProtocol: %s\nUser: %s\nAccess: shared writer\nSession: %s\nShortcuts: control %s, status/switch %s, quit %s, break %s, interrupt %s\nConnected at: %s" , remote . host , remote . port , config . channel , config . number , config . operation . Mode , actor , sessionID , shortcuts . control . label ( ) , shortcuts . switchSession . label ( ) , shortcuts . quit . label ( ) , shortcuts . sendBreak . label ( ) , shortcuts . interrupt . label ( ) , timestamp . Format ( time . RFC3339 ) )
}
type terminalShortcut struct {
notation string
value byte
enabled bool
}
func ( s terminalShortcut ) matches ( value byte ) bool { return s . enabled && s . value == value }
func ( s terminalShortcut ) label ( ) string {
if ! s . enabled {
return "disabled"
}
return s . notation
}
type terminalShortcuts struct {
control terminalShortcut
switchSession terminalShortcut
quit terminalShortcut
sendBreak terminalShortcut
interrupt terminalShortcut
}
func terminalShortcutSet ( operation domain . PortOperation ) terminalShortcuts {
return terminalShortcuts {
control : makeTerminalShortcut ( operation . ControlShortcut , "^]" ) ,
switchSession : makeTerminalShortcut ( operation . SwitchShortcut , "^T" ) ,
quit : makeTerminalShortcut ( operation . QuitShortcut , "^Q" ) ,
sendBreak : makeTerminalShortcut ( operation . BreakShortcut , "" ) ,
interrupt : makeTerminalShortcut ( operation . InterruptShortcut , "" ) ,
}
}
func makeTerminalShortcut ( notation , fallback string ) terminalShortcut {
notation = strings . ToUpper ( strings . TrimSpace ( notation ) )
if notation == "" {
notation = fallback
}
if len ( notation ) != 2 || notation [ 0 ] != '^' || notation [ 1 ] < '@' || notation [ 1 ] > '_' {
return terminalShortcut { }
}
return terminalShortcut { notation : notation , value : notation [ 1 ] & 0x1f , enabled : true }
}
func controlModeHelp ( shortcuts terminalShortcuts ) string {
return fmt . Sprintf ( "control mode: T resume/status, Q close, B send BREAK, I interrupt, ? help (direct: switch %s, quit %s, break %s, interrupt %s)" , shortcuts . switchSession . label ( ) , shortcuts . quit . label ( ) , shortcuts . sendBreak . label ( ) , shortcuts . interrupt . label ( ) )
}
type endpoint struct {
host string
port int
}
func remoteEndpoint ( address net . Addr ) endpoint {
if address == nil {
return endpoint { host : "unknown" }
}
host , portText , err := net . SplitHostPort ( address . String ( ) )
if err != nil {
return endpoint { host : address . String ( ) }
}
port , _ := strconv . Atoi ( portText )
return endpoint { host : strings . Trim ( host , "[]" ) , port : port }
}
func remoteEndpointString ( host string , port int ) ( string , int ) { return host , port }
func operationTimeout ( operation domain . PortOperation ) time . Duration {
return 10 * time . Second
}
func isManagedAuthentication ( method string ) bool {
switch strings . ToLower ( strings . TrimSpace ( method ) ) {
case "local" , "radius" , "tacacs" :
return true
default :
return false
}
}
func ( g * Gateway ) authenticate ( ctx context . Context , method , username , credential , channel string ) ( string , error ) {
if g . options . AuthenticateMethod != nil {
return g . options . AuthenticateMethod ( ctx , method , username , credential , channel )
}
if g . options . Authenticate != nil {
return g . options . Authenticate ( ctx , username , credential , channel )
}
return "" , errors . New ( "authentication service is unavailable" )
}
func credentialMatches ( expected , actual string ) bool {
if len ( expected ) != len ( actual ) || len ( expected ) == 0 {
return false
}
return subtle . ConstantTimeCompare ( [ ] byte ( expected ) , [ ] byte ( actual ) ) == 1
}
func operationNewline ( mode string ) [ ] byte {
switch mode {
case "lf" :
return [ ] byte { '\n' }
case "crlf" :
return [ ] byte { '\r' , '\n' }
default :
return [ ] byte { '\r' }
}
}
func readCredentialLine ( reader * bufio . Reader , limit int ) ( string , error ) {
line , err := reader . ReadString ( '\n' )
if err != nil {
return "" , err
}
line = strings . TrimRight ( line , "\r\n" )
if len ( line ) == 0 || len ( line ) > limit {
return "" , errors . New ( "credential line is invalid" )
}
return line , nil
}
func normalizeNewlines ( payload [ ] byte , mode string ) [ ] byte {
if mode == "" || mode == "none" {
return append ( [ ] byte ( nil ) , payload ... )
}
result := make ( [ ] byte , 0 , len ( payload ) * 2 )
for index := 0 ; index < len ( payload ) ; index ++ {
value := payload [ index ]
if value == '\r' && index + 1 < len ( payload ) && payload [ index + 1 ] == '\n' {
index ++
value = '\n'
}
if value != '\r' && value != '\n' {
result = append ( result , value )
continue
}
switch mode {
case "cr" :
result = append ( result , '\r' )
case "lf" :
result = append ( result , '\n' )
case "crlf" :
result = append ( result , '\r' , '\n' )
}
}
return result
}
// telnetReader removes option negotiation bytes before they can reach a UART.
// The parser state is retained because IAC commands and subnegotiation frames
// are routinely split across TCP reads.
type telnetReader struct {
reader io . Reader
state uint8
}
const (
telnetData uint8 = iota
telnetIAC
telnetOption
telnetSubnegotiation
telnetSubnegotiationIAC
)
func ( r * telnetReader ) Read ( output [ ] byte ) ( int , error ) {
if len ( output ) == 0 {
return 0 , nil
}
temporary := make ( [ ] byte , len ( output ) )
for {
count , err := r . reader . Read ( temporary )
written := 0
for _ , value := range temporary [ : count ] {
switch r . state {
case telnetData :
if value == 255 {
r . state = telnetIAC
} else if written < len ( output ) {
output [ written ] = value
written ++
}
case telnetIAC :
switch value {
case 255 :
if written < len ( output ) {
output [ written ] = value
written ++
}
r . state = telnetData
case 251 , 252 , 253 , 254 :
r . state = telnetOption
case 250 :
r . state = telnetSubnegotiation
default :
r . state = telnetData
}
case telnetOption :
r . state = telnetData
case telnetSubnegotiation :
if value == 255 {
r . state = telnetSubnegotiationIAC
}
case telnetSubnegotiationIAC :
if value == 240 {
r . state = telnetData
} else if value != 255 {
r . state = telnetSubnegotiation
}
}
}
if written > 0 {
return written , nil
}
if err != nil {
return 0 , err
}
}
}