ncat -lk -e cmd 127.0.0.1 8000
з попереднього посту, яке може
слухати на 127.0.0.1:8000 і форкати cmd (під'єднуючи діскріптора 0
(stdin) cmd до сокету для для читання та діскріптора 1 (stdout) для
писання ув нього), не є чимось оригінальним: саме так працював
супер-пупер-сервер inetd або Бернштайновський tcpserver.
(Кожного разу коли хтось посилається на Бернштайна, я згадую його подорожній нарис як його труїли газом ув готелі СПб літом 2000 року.)
З появою т.з. сокет октивації ув systemd, деякі контори (ойбіем) перестали (x)inetd поставляти взагалі: користувачам залишили або переписування файлів конфігурації inetd ув юніти systemd, або плигання на крижину убунту.
Корисний аспекта від цього був у тому, що супер-пупер-сервер inetd
хоча міг зміювати юзера у форку, сам запускався під рута і
конфігурацію тримав ув /etc (що вимагало права адміністратора для ігр зі своїми експериментальними
сервісами), а systemd запровадив режима
--user
і міг шукати юніти ув ~/.config/systemd/user/
.
Наприклад, складний мережевий сервіс, який питає ім'я клаенту і вітається з ним:
$ cat hello.sh
#!/bin/sh
uname 1>&2
while [ -z "$name" ]; do
printf "What is your name? "
read -r name || exit 1
done
echo "Hello, $name!"
можна писати з 0м рядків коду які оперують сокетами, а замість того додати 2 юніт файла:
$ cat ~/.config/systemd/user/hello.socket
[Unit]
Description=hello.sh socket
[Socket]
ListenStream=12345
Accept=yes
[Install]
WantedBy=sockets.target
та
$ cat ~/.config/systemd/user/hello@.service
[Unit]
Description=hello.sh
[Service]
ExecStart=-/home/alex/lib/software/example/ruby/fork/hello.sh
StandardInput=socket
Після чого:
$ systemctl --user daemon-reload
$ systemctl --user start hello.socket
$ ncat 127.0.0.1 12345
Linux
What is your name? Dude
Hello, Dude!
1й рядок з "Linux" з'явився тому що systemd по-замовчуванню під'єднує діскриптора 2 до сокету також, що я вважаю неввічливим.
Замість ncat тут краще перевіряти socat'ом, т.я. ncat не закриває з'єднання після того як systemd закрив сокета і продовжує висіти ув CLOSE_WAIT:
$ netstat -4an | grep 12345
tcp 0 0 127.0.0.1:34552 127.0.0.1:12345 CLOSE_WAIT
(Так само дивно поводиться nc ув busybox; що робить nc з bsd перевіряти лінь.)
Кому подобається писанина з ini-файлами є ув захваті. Всім іншим залишається журитися та писати форкові або тредові сервери вручну.
Емулювати ncat -lk
можна також на Рубі, де аналога вдається втиснути
ув 37 рядків коду. Той самий діалог
$ socat - TCP4:127.0.0.1:8000
What is your name? Dude
Hello, Dude!
Зі сторони сервера виглядає так:
$ ./tcplol -v -p 8000 ./hello.sh
Client 127.0.0.1:36512
Linux
Client 127.0.0.1:36512, pid 42528: disconnect
stderr під'єднується до сокету тільки з опцією -2
.
$ cat tcplol
#!/usr/bin/env ruby
require 'optparse'
require 'socket'
usage = 'Usage: tcplol [-2v] [-h 127.0.0.1] -p 1234 program [args...]'
opt = ARGV.getopts "h:p:2v"
abort usage unless opt['p'] && ARGV[0]
$VERBOSE = nil unless opt['v']
at_exit do
Thread.list.filter {|t| t != Thread.current}.each do |t|
warn "\n" + "Waiting for " + t[:cid]
t.join
end
end
server = TCPServer.new opt['h'] || '127.0.0.1', opt['p']
loop do
begin
client = server.accept
cid = client.remote_address.ip_unpack.join ':'
rescue
warn "Client error: #{$!}"
next
end
warn "Client #{cid}"
pid = fork do
$stdin.reopen client
$stdout.reopen client
$stderr.reopen client if opt['2']
client.close
exec(*ARGV)
end
client.close
cid += ", pid #{pid}"
Thread.new(cid, pid) do
Thread.current[:cid] = cid
Process.wait pid
warn "Client #{cid}: disconnect"
end
end
Щоб не залишати зімбі нам доводиться створювати окремого треда для кожного форку. at_exit
потрібен лише коли tcplol спілкується з клаентами, але отримує щось на кшталт SIGINT.
Єдина цікава деталь тут є ув чеканні на іксепшона поряд
з server.accept
. Не знаю чому, але я вважав що лайнаксне ядро деталі
TCP handshake ховає та унеможливлює його ламання з сокетного інтерхфейсу (але не з raw sockets, звичайно). Еге. Як прибрати
begin…rescue
, tcplol гепнеться на спробі дізнатися адресу хоста
клаента-злодія, якщо ув tcplol штирнути nmap'ом:
$ nmap -sT -p 8000 127.0.0.1