PRJEDU_telnet — a shell into a running engine¶
Every file of this example — the XML, the scripts, the page — is in
PRJEDU_telnet on GitHub.
Run it with run.bat (Windows) or ./run.sh (Linux) from that folder.
A plant that runs on its own, and a maintenance shell you can log in to while it does. Two windows:
run.bat
shell.bat plain telnet, 127.0.0.1:2323
shell.bat -Tls the same shell over TLS, 127.0.0.1:2324
On Linux: ./run.sh, then ./shell.sh (plain) or ./shell.sh --tls in a second terminal
(shell.py, Python 3; without python3 a plain telnet 127.0.0.1 2323 does too).
Log in as admin / admin123. Stop the engine with CTRL-C; leave the shell
with exit.
AyXDB.exe has a maintenance prompt behind CTRL-X, but that prompt reads
stdin — once the engine runs as a Windows service there is no console to type
into. PRO.telnet is the same capability on a socket, and because it is an
ordinary protocol it inherits the transport, TLS, the address filter and the
per-session role from the connection layer every other protocol already uses.
What you should see¶
AyMON example plant - maintenance shell
login: admin
password:
user 'admin', role 7, document 'SHL/S00' - type 'help'
SHL/S00> plant
level 30 %, pump off, mode auto, step 16 (you are admin, role 7)
SHL/S00> evt.GetValue("id('LEVEL')/@sta")
0x0343
SHL/S00> ver
platform : 4.3.4 build 446
document : SHL/S00
log file : C:\AyMON\AyMON\PRJEDU_telnet\log\SHL-20260922_235809.log
licence : AyMON EDU, 'AyMON education and personal use', lic_id AKYA-2026-000002 -- OKY
limits : doc 2/4 con 2/8 ses 0/32 cli 1/4 (refused: doc 0 con 0 ses 0 cli 0)
SHL/S00> log on 4
log follow ON: SHL/S00, level <= 4 ('log off' to stop)
SHL/S00>
|2026-09-22 22:23:41.118|6310|WARN | SHL/S00:LEVEL sta 0x0343 -> 0x0347 value 19.7 %
The files¶
| File | What is in it |
|---|---|
db\AyXDB.xml |
the root document: names the engine, loads the child |
db\db_SHL.xml |
the plant, the <auth> element, and the two shells |
db\db_SHL.js |
the simulator, and the pro_oncmd command hook |
shell.ps1 |
a terminal for the shell — plain or TLS, nothing to install |
shell.bat |
runs it with PowerShell's execution policy relaxed for that one process |
db\cert\ |
a public self-signed certificate for the TLS half |
What it teaches¶
- the shell is not privileged. A command runs with the session's own
role, so
xdb_aclapplies exactly as it does to any other remote caller. This makes it more restricted than the CTRL-X console, which runs as SYSTEM and sees everything. Granting somebody a shell grants them their role — no more, and no less. - three gates, and they are different gates.
con_acldecides who may open a socket,con_authwho may log in and as what,xdb_aclwhat that role may then touch. Each is worth having; none replaces another. - fail-closed. With neither
con_authnorpro_onloginthe shell refuses everyone and says so at startup.PRO.mcptreats a missing key as "authentication disabled"; that default is deliberately not repeated here, because this shell evaluates arbitrary script under the service account. - one
<auth>element serves every channel. Users, hashes, groups, lockout and the audit line live in one place; telnet, MQTT and a web login all point at it withcon_auth. - TLS is one attribute.
[CON.tls,PRO.telnet]instead of[CON.tcp,PRO.telnet]pluscon_ssl_*. The protocol code is identical. pro_oncmdadds project verbs. Return a value to answer the command, return nothing and the line falls through to the script engine — which is howplant,pump onandpinglive beside1+1.- the log is a stream, not just a file.
log on 4follows the current document's log live, per session, with its own level, without changing what anybody else sees.
Who may do what¶
Four users, and the roles are the whole point:
| Login | Password | Role | |
|---|---|---|---|
admin |
admin123 |
7 Admin | everything, including SECRET |
eng |
eng123 |
6 Engineer | everything except reading SECRET; may set MAINT |
opr |
opr123 |
3 Operator | may drive the pump while MAINT is 1 |
mon |
mon123 |
2 Monitor | may look |
The same three lines, logged in as different people:
SHL/S00> evt.GetValue("id('SECRET')/@val")
the boiler-room key is under the mat <- admin
<- opr: nothing at all
SHL/S00> evt.SetValue("id('MAINT')/@val", 1)
1 <- eng: written
0 <- opr: refused, and it stays 0
SHL/S00> pump on
pump on <- opr, once MAINT is 1
pump: role 2 may not touch the pump <- mon
Nothing in PRO.telnet decided any of that. SECRET carries
xdb_acl="0xF0000000" and MAINT carries 0xFF111111; the same answers come
back over MQTT, REST or a replication link. One nibble per role, role N in
bits 4N..4N+3, written high role first — 1 read, 2 write, 4 create,
8 delete.
LEVEL has no xdb_acl, so every logged-in role may write it, mon
included. That is not an oversight in the example — it is what "no ACL" means.
A role is worth nothing against elements that carry no ACL.
The filter is not per command, either. evt.GetValue("json(//var)") as
admin returns six elements; as opr it returns five, and SECRET is not
one of them — it is dropped from the array, not blanked out.
What the log keeps¶
Every attempt, accepted or not, leaves an audit line — the <auth> element
writes one and the shell writes the other:
AUTH(SHL/S00/A_SHELL): telnet 'admin'@127.0.0.1 ACCEPTED, role 7 via local
CN_TEL:PRO.telnet(1).login: 'admin' accepted, role = 7
AUTH(SHL/S00/A_SHELL): telnet 'opr'@127.0.0.1 REFUSED: wrong password
CN_TEL:PRO.telnet(1).login: 'opr' REFUSED (rc = 1)
A write the ACL turned down is in there as well, at ALERT — that one is an engine message and is in Turkish ("xdb_acl refused the write, role 3"):
SHL/S00:MAINT.val: xdb_acl yazma reddetti (rol 3)
Failed logins¶
Three wrong passwords close the socket (pro_tel_try="3"). Five failures for
the same user from the same address lock that user for 30 seconds
(auth_try="5" auth_lock="30"), across connections — the fourth session gets
too many failed attempts - try again later even with the right password.
An Admin elsewhere can clear it with AUTH.unlock("*").
Commands¶
Built in: help, help <SCOPE>, who, ver, history, pwd / cd NAME,
log [lvl] [lines], log on [lvl] / log off, exit.
From pro_oncmd in db_SHL.js: plant, pump on, pump off, ping.
Anything else is evaluated as script in the current document:
SHL/S00> 1+1
2
SHL/S00> evt.GetValue("json(//var)") a subtree as JSON
SHL/S00> evt.GetValue("stat(id('CN_TEL'))") runtime counters of a <con>
SHL/S00> evt.SetValue("#xdb_log", 7) engine log level, 0..7
SHL/S00> Object.XDB_EVT.help the full evt API
cd moves between documents, and log, log on and ver follow it: every
document keeps its own log file, so cd XDB then log 4 3 reads the root
document's log, not this one's.
Making your own passwords¶
admin carries a real PBKDF2-SHA256 hash; the other three are plain text,
which the engine accepts and quietly upgrades to a hash at the first
successful login — in memory only, since it cannot rewrite the file. Make
your own in the shell:
SHL/S00> AUTH.hash("the password")
$pbkdf2-sha256$100000$jXsn3BVlyDqfZP202ccXQw$R_z6y6IeImQVexOdxplKT4SXvNX3ye1TBZLKKnsaMjU
SHL/S00> AUTH.hash("the password", 20000) fewer iterations, for a slow box
and paste the answer into usr_pwd. One caveat: at xdb_log="7" the engine
dumps every received buffer, so the line you typed is in the log. The login
itself is never dumped — the password line is redacted — but a command you
type is not a password prompt.
Clients¶
shell.ps1 (shell.bat runs it with the execution policy relaxed for
that one process) is here because Windows 11 ships without a telnet client and
the obvious substitutes are poor. It runs in character mode, so the
shell's own line history (Up/Down), in-line editing and Ctrl-C line-cancel all
work, and it decodes UTF-8 statefully so engine messages come out right.
shell.bat |
plain, 127.0.0.1:2323 |
shell.bat -Tls |
TLS, 127.0.0.1:2324 |
shell.bat -Server 10.40.1.5 -Port 2323 |
somewhere else |
shell.bat -Tls -Verify |
validate the certificate (fails on the demo one, which is self-signed) |
PuTTY works too — connection type Telnet, not Raw and not SSH. In
Telnet mode it answers the ECHO and SGA options the shell announces, which is
what hides the password and puts the terminal in character mode. In SSH mode
PuTTY sends SSH-2.0-PuTTY..., the shell reads it as a user name, and the
session closes after three attempts — this shell speaks telnet, not SSH.
Windows' own telnet is an optional feature: dism /online
/Enable-Feature /FeatureName:TelnetClient from an elevated prompt.
openssl s_client -connect 127.0.0.1:2324 -quiet is enough for a quick
look at the TLS port. Keep -quiet: without it s_client eats the first
letter of every line as one of its own commands (C reconnects, Q quits),
so CON.list never reaches the shell.
Opening it to another machine¶
Both listeners are on 127.0.0.1 and con_acl="127.0.0.1" refuses everyone
else at accept time. Before you change that:
- [ ] use the TLS connection. Plain telnet carries the password, and everything after it, in clear text
- [ ] replace
db\cert\with a certificate that is actually yours. The one here is self-signed and its private key is in this repository - [ ] widen
con_aclonly as far as needed —10.40.1.*, not blank - [ ] hash the passwords, and give each user the lowest role that works
- [ ] put
xdb_aclon what matters; a role means nothing against elements that carry none - [ ] keep
con_idletmo, andcon_sessmall — this is not a user interface
If SSH is what you want, take it from something that already implements it:
run the plain listener on loopback and reach it through the Windows OpenSSH
server, or through ssh -L 2323:127.0.0.1:2323 user@host.
Known limit¶
The shell lives inside the engine. If the document is stopped or the
<con> is disabled, the shell goes down with it. It is a maintenance channel
for a running engine, not a rescue channel for a wedged one.
Licence¶
Two connections, two documents, one logged-in user: well inside FREE. Telnet is a FREE feature — a maintenance shell is not something you should have to buy to diagnose an installation.