I was able to get a hold of the DNP3 spec late last night, and that helped
tremendously to put some teeth in my understanding of the protocol. I
read up on the Virtual Terminal Objects section, and while am not nearly
an expert, have a beginning working knowledge.
I'll dive into the two links you provided and start working my way through
it. I'm sure more questions will come soon. :-)
Thanks!
Chris
I've been perusing through the Objects.h file, and see the Group80Var1 and
Group[110-113]Var0 definitions around line 1019
(https://github.com/gec/dnp3/blob/master/DNP3/Objects.h#L1019). From the
DNP3 standard in section 5.2.2, am I to understand that these are the
relevant objects for the job? I believe that object group 112 is for
master -> outstation flow, and object group 113 is for outstation ->
master flow. Does the presence of these definitions, however, indicate
that VTO support might already exist? Or at least, do most of the
required components already exist?
The ideal implementation would be compatible with Section 5.2.4
"Discontinuous octet streams," as it would allow for maximum compatibility
with protocols riding on top of the VTO connection.
Any thoughts on the number of maximum octets per message as per Section
5.2.6? A customizable macro seems like the best option, so that different
implementations can simply override with their desired size.
Thanks,
Chris
On 4/15/11 11:51 AM, "Adam Crain" <jadam...@gmail.com> wrote:
Hi Sam,Thanks for the explanation! Any help you can give on identifying the original port work would be extremely appreciated.I agree with you that a "router" component is needed, but have further questions about what it might look like. In my mind, this "router" component is very similar to htun, a Linux application (http://linux.softpedia.com/get/System/Networking/HTun-14751.shtml). htun uses two libraries external to itself — curl and sockets — to create an HTTP-based VPN tunnel from the client to the server. The entire IPv4 packet received from the client socket side (outstation) would be encoded into an HTTP request and then sent over HTTP to the concentrator (master) on the other side. The master would then decode the HTTP request and pass on the IPv4 packet to its final destination. The actual application (VPN tunnel) is decoupled from the two underlying libraries (curl's HTTP implementation and Linux's network sockets implementation.) A similar system (dnp3tun, if you will?) might provided similar benefits, whereby standard IP protocols like SSH and HTTP could be used across a DNP3 network backbone. And Linux applications could use the standard TUN/TAP virtual networking device to quickly and easily add support to existing network daemons for DNP3 through this tunnel/router system.A little bit of protocol framing might be required to ensure that fragmented packets are reassembled properly before being forwarded. HTTP provided that implicitly in the htun example above, but we can develop that higher-level control protocol later.Three questions that come to mind out of our discussion at this point:
- Is the implementation of the socket-to-VTO router component something that is best done outside or inside of the DNP3 library?
- If outside, then are there any special API hooks in the DNP3 library needed by the router?
- Are there any "standards" (de facto or de jure) in this area that we should be keeping in mind, such as DNP3 addressing conventions, IP-to-DNP3 address mappings, etc.?
index 423ea82..aae0bd4 100644--- a/DNP3/AsyncEventBuffers.h+++ b/DNP3/AsyncEventBuffers.h@@ -61,6 +61,11 @@ namespace apl { namespace dnp {public:AsyncInsertionOrderedEventBuffer(size_t aMaxEvents);++ void _Update(const EventType& arEvent);+ private:++ int_32_t mNextEvent;};@@ -76,7 +81,8 @@ namespace apl { namespace dnp {template <class EventType>AsyncInsertionOrderedEventBuffer<EventType> :: AsyncInsertionOrderedEventBuffer(size_t aMaxEvents) :- AsyncEventBufferBase<EventType, InsertionOrderSet2< EventType > >(aMaxEvents)+ AsyncEventBufferBase<EventType, InsertionOrderSet2< EventType > >(aMaxEvents),+ mNextEvent(0){}template <class EventType>@@ -99,6 +105,26 @@ namespace apl { namespace dnp {}}+ template <class EventType>+ void AsyncInsertionOrderedEventBuffer<EventType> :: _Update(const EventType& arEvent)+ {+ if (this->mEventSet.size() == 0 && this->mSelectedEvents.size() == 0)+ mNextEvent = 0;++ /*+ * When the object is first inserted, this flag will be 0, so we set it+ * to the next value. This allows us to guarantee that the set keeps+ * the events in the correct order when we put the selected events+ * back in the main buffer on failures.+ */+ if (arEvent.mLastEventValue == 0) {+ const_cast<EventType&>(arEvent).mLastEventValue = ++mNextEvent;+ }++ this->mEventSet.insert(arEvent);+ this->mCounter.IncrCount(arEvent.mClass);+ }+}} //end NS#endifindex fa8e0e8..5f24638 100644--- a/DNP3/EventTypes.h+++ b/DNP3/EventTypes.h@@ -34,13 +34,15 @@ struct EventInfo : public PointInfoBase<T>EventInfo(const T& arValue, PointClass aClass, size_t aIndex) :PointInfoBase<T>(arValue, aClass, aIndex),mSequence(0),- mWritten(false)+ mWritten(false),+ mLastEventValue(0){}- EventInfo() : mSequence(0), mWritten(false) {}+ EventInfo() : mSequence(0), mWritten(false), mLastEventValue(0) {}size_t mSequence; /// sequence number used by the event buffers to record insertion orderbool mWritten; /// true if the event has been written+ size_t mLastEventValue; /// the last event value if using an insertion ordered buffer};typedef EventInfo<apl::Binary> BinaryEvent;
Done! I've never actually used Github, very cool interface. Fork is
available from https://github.com/cverges/dnp3.
Thanks,
Chris
Applied. I was having some Internet issues, so the email went out before
the commit. Apologies for the weird timing.
While you're reviewing, if real-time IM is easier, I'm on GTalk. Feel
free to reach out if needed.
Thanks for the help!
Chris
Very welcome! The VTO port that Sam sent was extremely helpful in doing
the rote work involved.
Regarding your suggestion, I like it. A basic skeleton might be helpful
in making sure that I understand the structure desired. I'll hack
something together and commit it to the fork later today.
By the way, is there a Developer API Reference that discusses the purposes
and relationships of all the various classes involved? I'm planning to
add a significant amount of doc work to the VTO classes involved in this
new effort, but want to make sure that this work is stylistically related
to anything that might already exist.
Thanks,
Chris
OK Step 1 should be done. I pushed updates to the fork that should remove
the VtoData portion from IDataObserver, ChangeBuffer, Database, Slave,
etc. This is in preparation for the second step of creating skeleton code
for the reader/writer handlers.
Also, I updated the doxygen config file to exclude the tinyxml and boost
source files, since they're outside the DNP3 code base. I wasn't sure if
these sources were included on purposes, but figured it was an easy thing
to revert. :-)
Thanks,
Chris
Sounds good. I'm also on PTO, so good to hear that the DNP3 group is
apparently where all the geeks go when we're not supposed to be geeking
out. :-)
Thanks,
Chris
I've been considering what API might be needed. As I understand the DNP3
protocol, VTO operates using Object Group 112 for master -> outstation and
Object Group 113 for outstation -> master. If that's the case, then will
we need to setup separate registration handlers?
Also, since the Virtual Channels feature of VTO will most likely involve
forking data streams into multiple directions, should we set it up so that
you register separate channels with separate handlers?
As you work on this more tomorrow, here's the code version of some of my
thoughts:
namespace apl { namespace dnp {
class AsyncStackManager {
public:
/**
* The virtual channel ID is defined when the application
* instantiates the apOnDataCallback object. A corresponding
* writer object will be created for that channel ID.
*/
IVtoMasterWriter* AddMasterVtoChannel(const std::string& arStackName,
IVtoMasterReader* apOnDataCallback,
size_t
reservedOctetCount = 0);
IVtoSlaveWriter* AddSlaveVtoChannel(const std::string& arStackName,
IVtoSlaveReader* apOnDataCallback,
size_t
reservedOctetCount = 0);
}
class IVtoHandler {
public:
IVtoHandler(uint_8_t channelId);
~IVtoHandler();
uint_8_t GetChannelId() { return channelId; }
private:
uint_8_t channelId;
}
class IVtoWriter : private IVtoHandler {
public:
void Send(const byte_t& data, size_t length);
size_t GetReservedOctetCount() { return reservedOctetCount; }
void SetReservedOctetCount(size_t count) { reservedOctetCount = count; }
private:
/**
* The reserved octet count is used as a dynamic throttle control
* mechanism. In any outbound DNP3 Application Layer message,
* a corresponding number of octet will be reserved for VTO-related
* data. Note that this reserved count corresponds to the VTO
* data only, and does not include the object header information.
*
* VTO is considered a "low-priority" message type, and could easily
* overwhelm the DNP3 application layer. Such an event could result
* in time-sensitive data (such as point values) being dropped or
* become useless. Barring the creation of a more generic Quality
* of Service (QoS) mechanism, this is a first attempt at that.
* The VTO data will be dropped or postponed if other data is
* in the queue, waiting to be sent. To ensure that SOME VTO data
* sneaks through, however, you can reserve a certain number of
* octets in the application layer for VTO.
*/
size_t reservedOctetCount;
}
class IVtoReader : private IVtoHandler {
public:
/**
* Blocks until data is received, then fills the data buffer and
* sets the length of the data received (up to maxLength). If
* block is set to false, the call does not block. The function
* returns the number of bytes stored into the data buffer.
*/
size_t Recv(const byte_t& data,
size_t maxLength,
bool block = true);
}
class IVtoMasterWriter : private IVtoWriter { ... }
class IVtoSlaveWriter : private IVtoWriter { ... }
class IVtoMasterReader : private IVtoReader { ... }
class IVtoSlaveReader : private IVtoReader { ... }
}
The existing RemoveStack() should be able to process the removal of the
VTO stacks. And I agree, using the io_service makes a lot of sense.
I'll merge the above code into my fork later today so that it's ready for
you tomorrow.
Looking forward to hearing your thoughts!
Thanks,
Chris
Consolidated API to enforce usage pattern.The API should be called in the following order (example shown):1. AsyncStackManager::AddSerial(portName)2. AsyncStackManager::AddMaster(portName, stackName)3. AsyncStackManager::AddVtoChannel(stackName, channelId)4. StartVtoRouter(stackName)The VtoRouter exists on the stack-level, which has already beenassociated with a port (serial, TCP, etc.) As such, the only remainingitem is to define which virtual circuit ID will be used for the stack.Then, of course, start the router.
Umm, then I don't think I quite understood. My impression was that the
two worked together. So in the [Add|Remove]VtoChannel scenario, how do
the DNP3 data link through application layers work? And in the
[Start|Stop]Router scenario, how does the client code publish and receive
the VTO data?
Thanks,
Chris
So the equivalent would be as follows?
[Outstation] <---(tcp/ip)---- [Master 12,345]
[TCP 20,000] [ | ]
[ Router ]
[SSH Client] [ | ]
[TCP 23,456] ----(tcp/ip)---> [ Master 22 ]
I guess my initial thoughts were that the "router" functionality is
something completely separate from the library, so didn't expect to find
it embedded in the DNP3 directory. Maybe like a "DNP3Router" subproject.
But OK, this explanation has helped me to understand things better.
So StartVtoRouter() will create its own set of IVto[Writer|Callbacks] and
manage things, right? For actual masters and oustations that need to
interpret the VTO streams, they would make their own
Ivto[Writer|Callbacks] instances and go from there.
Does this interpretation jive with the library's actual usage?
Thanks,
Chris
OK, I'll revert the changes in my fork either tomorrow or Monday. We
should be ready for another code skeleton review by Monday afternoon.
Thanks for your patience and help in explaining all this.
The exact protocol hasn't been selected yet. Obviously, support for the
standard set would be nice (IPv6 as a generic stack, HTTP, FTP, RSYNC,
etc.)
Regarding the master connecting with multiple outstations, I know we
discussed this in some depth on Friday. However, in thinking about it a
little more today, I may have confused myself again. :-) I'll keep
thinking about it and post something more succinct next week.
Thanks,
Chris
Added VtoMasterWriter and VtoSlaveWriter classes. We will need to create the appropriate instance during AsyncStackManager::AddVtoChannel() depending on the stack type defined by arStackName.
Fixed small syntax errors.
Removed arPortName from AddVtoChannel() since this function terminates the VTO stream for a stack. Changed StopVtoRouter() declarations to operate based on the arStackName, not the arPortName.
Thanks for the response! Addition follow up inline as well ...
On 4/25/11 7:42 AM, "Adam Crain" <jadam...@gmail.com> wrote:
>On Apr 22, 6:14 pm, Chris Verges <chris.ver...@gmail.com> wrote:
>> Hi Adam,
>>
>> I'm still working my way through the various classes in the DNP3
>>directory
>> so that I know how to implement the VTO feature. Here's what I have so
>>far
>> in the way of understanding: (don't worry, it'll be short!)
>>
>> class APDU
>> * This class represents a DNP3 Application Layer message.
>> * WriteIndexed() seems to be the proper one to use for sending
>>Group112VarX
>> and Group113VarX objects.
>
>This is where my memory starts slipping. I just talked to Sam and he
>recalls that the index encoded the "port" of the VTO object, and the
>variation encoded the length (0-255). I believe that WriteIndexed()
>is the correct function, but you'll have to do some unit testing here
>and comparison to the protocol specification.
Let's get clear on terminology, since "port" seems to be overloaded
already. :-) As I understand it, a "port" in the GEC library is a
physical layer connection, such as a serial port or a TCP port. A
"virtual channel" in the DNP3 specification is similar to a TCP or UDP
port, allowing for multiplexing of the VTO mechanism. Does this match
with your understanding? If so, then I agree with your explanation if we
change "port" to "virtual channel."
>>class ObjectBase
>> * Get() creates the Group112VarX and Group113VarX objects, used by class
>> APDU.
>
>I'm not sure if you need this function or not. Just use the existing
>read/write handler for existing objects as a guide.
OK. Where can I find an example of this existing mechanism?
>> class IVtoWriter, class VtoMasterWriter, and class VtoSlaveWriter
>> * This class writes a byte stream from the application of arbitrary
>>length
>> to an APDU instance.
>> * The stream is split into segments of at most 255 characters. The
>>minimum
>> size can vary from 0 to aReservedOctetCount, as determined by the
>>available
>> space in the APDU instance. (Unclear how to ensure that
>>aReservedOctetCount
>> is made available if a VTO transmission is pending.)
>
>I think that aReservedOctetCount might be better as a Slave/Master
>configuration parameter (Minimum amount from ANY VTO stream to put
>into each APDU). It will be really hard to do this on a per stream
>basis.
Agreed. I will change the MasterConfig and SlaveConfig objects to reflect
this.
>>* Package each segment into a Group112VarX (if VtoMasterWriter) or
>> Group113VarX (if VtoSlaveWriter) instance.
>> class IVtoCallbacks
>> * This class notifies the implementing application of newly received
>>data.
>> * The application should then do whatever it needs with the data,
>>though it
>> should do it quickly, as this blocks other DNP3 functions from being
>> processed.
>> class Master
>> * Master::ProcessDataResponse() seems to process a received class APDU
>> instance.
>> * Various other functions like Master::OnUnsolResponse() seem to trigger
>> other paths, unclear if they eventually converge back to
>> Master::ProcessDataResponse().
>
>I believe that all of those functions return to ProcessDataResponse.
>This is where you'll read the VTO objects out of the APDU and convert
>them to array[byte].
OK. Master::ProcessDataResponse() will be the main change location, then.
>>class Slave
>> * Slave::Send() and Slave::SendUnsolicited() are used to tell class
>>AppLayer
>> to write the class APDU instance.
>> * Slave::CreateResponseContext() appears to create a class APDU
>>instance,
>> perhaps usable by class VtoSlaveWriter? (Does this mean that
>>Slave->Master
>> communications will be VTO-packets-only ‹ this is, the APDU instance
>>will
>> only hold VTO objects, no other types?)
>
>No, the responses from the slave (either polled or unsolicted) will
>contain a mixture of measurement data and VTO objects. The exact
>mixture will be a function of:
>
>1) VTO ReserverOctetCount
>2) Events/VTO available
>3) Maximum size of an APDU
>
>> * How does the class Slave instance receive VTO data?
>
>Masters write VTO objects to the outstation using the FC_WRITE
>function. You'd have to add a handler here:
>
>https://github.com/gec/dnp3/blob/master/DNP3/Slave.cpp#L301
Sounds good! So I'll need to add some case(MACRO_DNP_RADIX(112,0))
statements or the proper equivalent to the Slave::HandleWrite() function.
Thanks,
Chris
aReservedOctetCount moved to MasterConfig and SlaveConfig. The VTO
reserved octet count is now specified on the stack level for VTO as a
whole; each virtual channel will need to share this reserved count.
Chris
On 4/25/11 7:42 AM, "Adam Crain" <jadam...@gmail.com> wrote:
Based on this and our IM conversation, I started looking at TestAPDU.cpp
and TestAPDUWriting.cpp to see what modifications to test cases would be
needed. Please see the following for further details.
In short, the current behavior of the APDU class on line 250
(https://github.com/cverges/dnp3/blob/master/DNP3/APDU.cpp#L250) is such
that only one VTO objects (either Groups 112 or 113) is allowed in a
single APDU message during the parsing operation. However, we can write a
DNP3 message such that multiple VTO packets can be sent.
What is the expected behavior? As best as I understand the DNP3 spec,
there is no limitation on the number of VTO objects allowed in a DNP3
application layer message. If that is the case, then it seems like
APDU.cpp needs to be modified to support this. Otherwise, is there
anything we can do on the writing-side to maintain consistency with the
APDU parser?
Thanks,
Chris
Relevant Commits:
https://github.com/cverges/dnp3/commit/8138a428227a2872aa062d8e04294813902a
8577
Added new test case VirtualTerminalWriteMultipleIndices to
TestAPDUWriting.cpp.
https://github.com/cverges/dnp3/commit/5547d13ee4d3106b80535e2707be59c7f478
a3ae
Added new test case VtoObjectBadWriteMultipleIndices and error code
ALERR_TOO_MANY_VARIABLE_OBJECTS_IN_HEADER.
Thanks for confirming that this is an artificial limitation. I've removed
the count check and added a new test case VtoObjectWriteMultipleIndices to
TestAPDU.cpp.
https://github.com/cverges/dnp3/commit/5e9f75571099b843f07d96fd486e0ea3c46e
81b4
Chris