Inspect UDP syslog packets in Ruby - ruby

I'm attempting to hack together a Ruby-based (1.9.1) syslog server, and am running into a pretty basic issue right from the get-go.
Here's my (very basic) code:
#!/usr/bin/env ruby
require 'socket'
require 'io/wait'
require 'syslog'
class Server
def initialize
#listener = UDPSocket.new
#listener.bind("192.168.253.5", "514")
getdata
end
def getdata
while true
#text, #sender = #listener.recvfrom(9000)
p #listener
p #text
p #sender
end
end
end
x = Server.new
It all works fine, except that this does not display either the facility or the severity of the message:
#<UDPSocket:fd 5>
"<189>49: *Mar 1 00:24:37.862: %LINEPROTO-5-UPDOWN: Line protocol on Interface FastEthernet0/8, changed state to down"
["AF_INET", 56970, "192.168.253.10", "192.168.253.10"]
Tcpdump shows this info just fine ("local7" facility, "notice" severity):
15:18:01.987542 IP 192.168.253.10.56970 > 192.168.253.5.514: SYSLOG local7.notice, length: 115
How can I inspect the UDP packet that was sent to me so I can glean both facility and severity of the syslog message?

Whenever you are implementing a well-defined network protocol, always look at the RFC:
https://www.rfc-editor.org/rfc/rfc5424
The Priority value is calculated by first multiplying the Facility
number by 8 and then adding the numerical value of the Severity.
so "local7" is 23 according to the RFC. 23 * 8 = 184
the severity of "notice" is 5: 184 + 5 = 189.
And there's 189 right at the beginning of your message - that's the "priority" number referenced by the RFC.
So you'll need to encode the mapping from the RFC between numeric values and the textual description into your program and compute it yourself.
To get the severify and facility:
Severity = Priority % 8
Facility = Priority / 8

Related

send email via telnet ruby script

I'm new with ruby and need to send an email via telnet using a relay host with no authentication. I can do it with a linux shell but I need to put it in a script so I can "simplify" its use, I know it's not the best way but I can't find other since the server where i'm working on it's severely restricted and limited.
require 'net/telnet.rb'
mail = Net::Telnet::new(
"Host" => "domain.ip", # default: "localhost"
"Port" => 25, # default: 23
"Output_log" => "output_log", # default: nil (no output)
"Dump_log" => "dump_log", # default: nil (no output)
"Prompt" => /[$%#>] \z/n, # default: /[$%#>] \z/n
"Telnetmode" => true, # default: true
"Timeout" => 10, # default: 10
"Waittime" => 0, # default: 0
)
mail.cmd('helo MYDOMAIN'){ |c| print c }
mail.cmd('mail from: test#domain.com')
mail.cmd('rcpt to: test2#domain.com')
mail.cmd('data')
mail.cmd("subject: test cmd \n\n mensaje de prueba\n\n")
mail.cmd(".\n")
mail.close
I found the net/telnet.rb ruby class and this is my try... after mail.cmd('helo MYDOMAIN') I can't keep writing other commands, what I get is:
220 mail.server.com ESMTP
250 mail.server.com
After this I'm suposed to write mail from, etc. to create the mail. But I can't in the ruby script. I have try using:
mail.puts('mail from: test...')
mail.write('mail from: test...')
mail.print('mail from: test...')
mail.cmd('mail from: test...')
As written in documentation
Also I don't get the telnetmode(true|false) command maybe you could explain it to me please.
-- Edit --
Shell code trying to emulate:
telnet domain.ip 25
#=> Trying domain.ip...
#=> Connected to domain.ip.
#=> Escape character is '^]'.
#=> 220 mail.server.com ESMTP
helo MYDOMAIN
#=>250 mail.server.com
mail from:test#mydomain.com
#=> 250 2.1.0 Ok
rcpt to:test2#mydomain.com
#=> 250 2.1.0 Ok
data
#=> 354 End data with <CR><LF>.<CR><LF>
subject: test mail
test mail body
.
#=> 250 2.0.0 =k: queued as B6F08480D12
quit
#=> 221 2.0.0 Bye
#=> Connection closed by foreign host.
The telnet protocol is really, really rudimentary which is why the telnet command is useful for testing TCP/IP based services such as SMTP or HTTP. It does not mean those services actually use the telnet protocol, as they don't. They're conveniently plain-text in nature which means it's practical to use telnet for simple tests.
You should not be using the Telnet module for anything other than connecting to telnet services, though given it's 2017 it's unlikely you'll find any of those around.
You should be using something like Socket to connect. This can create a bare TCP/IP connection with full control over sending. As this is a wrapper around a regular POSIX filehandle you can use all the IO methods on it for reading, writing, and other control functions, like a proper socket shutdown.
Writing an SMTP adapter is not as easy as it seems, there's a lot of tricky things to tackle with regard to IO. You'll need to use IO.select to properly test for new data, plus that the socket is clear to write your email.
Here's a new stub:
require 'socket'
mail = TCPSocket.new("smtp.example.com", 25)
mail.write("HELO example.com\r\n")
Another note is that when you call require you should never specify the file extension. It's always handled for you.
Thanks to the help of the user ddubs how suggest the net\smtp gem (One that I didn't know) I was able to create a simple mail sender and using the mailfactory gem
Is it a strict requirement that you use telnet? Using ruby-doc.org/stdlib-2.0.0/libdoc/net/smtp/rdoc/Net/SMTP.html will turn your "difficult to maintain" script into something that is much easier to maintain. Even for someone who is completely new to Ruby. – ddubs
Here is the code sample
require 'net/smtp'
require 'mailfactory'
mail_body_HTML = '<h1> mail title</h1> your text in <b>HTML</b>'
mail_body_PLAIN = 'this is plain text'
mail_subject = 'test email'
mail_from = 'noreply#mydomain.com'
mail_to = 'user#otherdomain.com'
# mail_filePath = ''
mail = MailFactory.new()
mail.to = mail_to
mail.from = mail_from
mail.subject = mail_subject
mail.html = mail_body_HTML
# mail.text = mail_body_PLAIN
# mail.attach(mail_filePath)
relay_ip = x.x.x.x
Net::SMTP.start(relay_ip,25) do |smtp|
smtp.send_message(mail.to_s, mail_from, mail_to)
end

Ruby, get incoming address from UDP message

I have a UDP server that binds to all addresses on a system, I would like to know what ip address the message was addressed to. Any ideas how to do this?
Here is my example code:
sock = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM, 0)
sock.bind(Addrinfo.udp('', 2400))
while(true)
sockset = IO.select([sock])
sockset[0].each do |sock|
data = sock.recvfrom(1024)
puts "data: " + data.inspect
end
end
sock.close
This will produce something like:
data: ["test message\n", #<Addrinfo: 172.16.5.110:41949 UDP>]
Am I able to set a socket option, or something, to return the local IP?
Just a note, this needs to work for IPv6 too. Thanks in advance, Dave.
UNIX Network Programming has this to say about this very subject:
With a UDP socket, however, the destination IP address can only be obtained by setting the IP_RECVDSTADDR socket option for IPv4 or the IPV6_PKTINFO socket option for IPv6 and then calling recvmsg instead of recvfrom.
Ruby’s socket library has recvmsg which is a bit easier to use than the underlying C function, but still needs a bit of work to get the info needed. The destination IP address is included in the array of ancillary data returned from recvmsg. Here’s a version of your code adapted to use recvmsg and get the destination address for IPv4:
require 'socket'
require 'ipaddr'
sock = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM, 0)
# Set the required socket option:
sock.setsockopt :IPPROTO_IP, :IP_RECVDSTADDR, true
sock.bind(Addrinfo.udp('0.0.0.0', 2400))
while(true)
sockset = IO.select([sock])
sockset[0].each do |sock|
mesg, sender, _, *anc_data = sock.recvmsg
# Find the relevant data and extract it into a string
dest = IPAddr.ntop(anc_data.find {|d| d.cmsg_is?(:IP, :RECVDSTADDR)}.data)
puts "Data: #{mesg}, Sender: #{sender.ip_address}, Destination: #{dest}"
end
end
And here is a version for IPv6. There is also a RECVPKTINFO socket option, which I think may have superseded PKTINFO – depending on your system you may need to use that instead.
require 'socket'
sock = Socket.new(Socket::AF_INET6, Socket::SOCK_DGRAM, 0)
# Set the socket option for IP6:
sock.setsockopt :IPPROTO_IPV6, :IPV6_PKTINFO, true
sock.bind(Addrinfo.udp('0::0', 2400))
while(true)
sockset = IO.select([sock])
sockset[0].each do |sock|
mesg, sender, _, *anc_data = sock.recvmsg
# Find and extract the destination address
dest = anc_data.find {|d| d.cmsg_is?(:IPV6, :PKTINFO)}.ipv6_pktinfo_addr
puts "Data: #{mesg}, Sender: #{sender.ip_address}, Destination: #{dest.ip_address}"
end
end
Ruby also provides a Socket.udp_server_loop method, which yields the message and a UDPSource object to the block you provide, and this source object has a local_address field. Looking at the source this appears to check the PKTINFO data like I do above to get the destination address for IPv6 requests, but not for IPv4. This method binds to all available IP addresses individually, and just uses the address of the incoming interface for IP4 requests, which may not be accurate for a weak end system model. However it might be simpler for you to use Socket.udp_server_loop.

Get wifi BSSID programmatically using Ruby and ioctl

Using Getting essid via ioctl in ruby as a template I wanted to get the BSSID rather than the ESSID. However, not being a C developer, there are a few things that I don't understand.
What I have so far which does not work :( ...
NOTE I'm a bit confused because part of me thinks, according to some comments in wireless.h, that the BSSID can only be set via ioctl. However, the ioctl to get exists. That along with my almost complete lack of understanding of the more intermediate C type isms (structs, unions, and stuff ;) ), I simply don't know.
def _get_bssid(interface)
# Copied from wireless.h
# supposing a 16 byte address and 32 byte buffer but I'm totally
# guessing here.
iwreq = [interface, '' * 48,0].pack('a*pI')
sock = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM, 0)
# from wireless.h
# SIOCGIWAP 0x8B15 /* get access point MAC addresses */
sock.ioctl('0x8B15', iwreq) # always get an error: Can't convert string to Integer
puts iwreq.inspect
end
So, in the meantime, I'm using a wpa_cli method for grabbing the BSSID but I'd prefer to use IOCTL:
def _wpa_status(interface)
wpa_data = nil
unless interface.nil?
# need to write a method to get the src_sock_path
# programmatically. Fortunately, for me
# this is going to be the correct sock path 99% of the time.
# Ideas to get programmatically would be:
# parse wpa_supplicant.conf
# check process table | grep wpa_suppl | parse arguments
src_sock_path = '/var/run/wpa_supplicant/' + interface
else
return nil
end
client_sock_path = '/var/run/hwinfo_wpa'
# open Domain socket
socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM, 0)
begin
# bind client domain socket
socket.bind(Socket.pack_sockaddr_un(client_sock_path))
# connect to server with our client socket
socket.connect(Socket.pack_sockaddr_un(src_sock_path))
# send STATUS command
socket.send('STATUS', 0)
# receive 1024 bytes (totally arbitrary value)
# split lines by \n
# store in variable wpa_data.
wpa_data = socket.recv(1024)
rescue => e
$stderr.puts 'WARN: unable to gather wpa data: ' + e.inspect
end
# close or next time we attempt to read it will fail.
socket.close
begin
# remove the domain socket file for the client
File.unlink(client_sock_path)
rescue => e
$stderr.puts 'WARN: ' + e.inspect
end
unless wpa_data.nil?
#wifis = Hash[wpa_data.split(/\n/).map\
{|line|
# first, split into pairs delimited by '='
key,value = line.split('=')
# if key is camel-humped then put space in front
# of capped letter
if key =~ /[a-z][A-Z]/
key.gsub!(/([a-z])([A-Z])/,'\\1_\\2')
end
# if key is "id" then rename it.
key.eql?('id') && key = 'wpa_id'
# fix key so that it can be used as a table name
# by replacing spaces with underscores
key.gsub!(' ','_')
# lower case it.
key.downcase!
[key,value]
}]
end
end
EDIT:
So far nobody has been able to answer this question. I think I'm liking the wpa method better anyway because I'm getting more data from it. That said, one call-out I'd like to make is if anyone uses the wpa code, be aware that it will require escalated privileges to read the wlan socket.
EDIT^2 (full code snippet):
Thanks to #dasup I've been able to re-factor my class to correctly pull the bssid and essids using system ioctls. (YMMV given the implementation, age, and any other possible destabilization thing to your Linux distribution - the following code snippet works with the 3.2 and 3.7 kernels though.)
require 'socket'
class Wpa
attr_accessor :essid, :bssid, :if
def initialize(interface)
#if = interface
puts 'essid: ' + _get_essid.inspect
puts 'bssid: ' + _get_bssid.inspect
end
def _get_essid
# Copied from wireless.h
iwreq = [#if, " " * 32, 32, 0 ].pack('a16pII')
sock = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM, 0)
sock.ioctl(0x8B1B, iwreq)
#essid = iwreq.unpack('#16p').pop.strip
end
def _get_bssid
# Copied from wireless.h
# supposing a 16 byte address and 32 byte buffer but I'm totally
# guessing here.
iwreq = [#if, "\0" * 32].pack('a16a32')
sock = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM, 0)
# from wireless.h
# SIOCGIWAP 0x8B15 /* get access point MAC addresses */
sock.ioctl(0x8B15, iwreq) # always get an error: Can't convert string to Integer
#bssid = iwreq.unpack('#18H2H2H2H2H2H2').join(':')
end
end
h = Wpa.new('wlan0')
I'm not very much familiar with Ruby, but I spotted two mistakes:
The hex number for SIOCGIWAP should be given without quotes/ticks.
The initialization of the data buffer ends up with some trailing bytes after the interface name (debugged using gdb). The initialization given below works.
Be aware that your code will break if any of the data structures or constants change (IFNAMSIZ, sa_family, struct sockaddr etc.) However, I don't think that such changes are likely anytime soon.
require 'socket'
def _get_bssid(interface)
# Copied from wireless.h
# supposing a 16 byte address and 32 byte buffer but I'm totally
# guessing here.
iwreq = [interface, "\0" * 32].pack('a16a32')
sock = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM, 0)
# from wireless.h
# SIOCGIWAP 0x8B15 /* get access point MAC addresses */
sock.ioctl(0x8B15, iwreq) # always get an error: Can't convert string to Integer
puts iwreq.inspect
end
You'll get back an array/buffer with:
The interface name you sent, padded with 0x00 bytes to a total length of 16 bytes.
Followed by a struct sockaddr, i.e. a two-byte identifier 0x01 0x00 (coming from ARPHRD_ETHER?) followed by the BSSID padded with 0x00 bytes to a total length of 14 bytes.
Good luck!

Pack a string with an uint34

I'm trying to send something via socket so I made a simple protocol which is :
[Message length (uint32)][Packet id(uint32)][Message]
So, how can I send a message with this protocol ? I tried this :
message = 'hi'
parent_socket.send([message.length, 2].pack('LL') + message, 0)
and it doesn't seems to work. What I get is just the length and the packet id.
How can I figure this out?
(Updated)
The code I'm using to receive the message is :
if child_socket.ready?
header = child_socket.read(8).unpack('LL')
length = header[0]
packet = header[1]
case packet
when 1
stdin.write(child_socket.read(length))
when 2
puts child_socket.read(length)
#send console
else
Console.show "Unknown packet : #{packet}"
end
The output is 10. Seems normal (4 + 4 + 2)
in that case, length is 2 and packet is 2. So it switch to 'when 2' but it output nothing.
The problem is you are only reading the first 8 byes.
header = child_socket.read(8).unpack('LL')
According to the docs for IO#read, if you don't pass a length, it will read to EOF, which should get everything. So just remove that length parameter:
header = child_socket.read.unpack('LL')

How to implement INSTREAM protocal for clamd in Ruby?

I am struggling to implement the INSTREAM command of clamd daemon in Ruby.
Here is the document of clamd
#file = File.open("input.txt")
socket = TCPSocket.new(HOST, PORT)
#writing the command
socket.write("zINSTREAM\0")
#streaming the chunk
socket.write(1024) #size of chunk
socket.write(#file.read(1024)) #chunk of data
#end the streaming
socket.write(0)
puts "Reading from the scoket"
puts socket.recv(1024)
socket.close
But I am always receiving the error response "INSTREAM size limit exceeded. ERROR"
What I doing wrong here?
After the long struggle I found the solution for this.
The size of the chunk must be expressed in 4 byte unsigned integer in network byte order
So
socket.write(1024)
should be
socket.write([1024].pack("N"))

Resources