From c9ed6bd04f03c2a57bcc129506e910e8f6879261 Mon Sep 17 00:00:00 2001 From: Jeff Weston Date: Wed, 11 Sep 2019 15:33:15 -0400 Subject: [PATCH] add norp v0.1b5 --- norp/TODO.txt | 37 + norp/VERSION.TXT | 29 + norp/doc/norpUserGuide.html | 23 + norp/doc/norpUserGuide.pdf | Bin 0 -> 126544 bytes norp/doc/norpUserGuide.xml | 567 ++++++ norp/doc/resources/ProteanLogo.png | Bin 0 -> 28382 bytes norp/doc/resources/norpConcept.png | Bin 0 -> 59309 bytes norp/include/norp.h | 644 +++++++ norp/include/norpVersion.h | 3 + norp/include/protoPktSOCKS.h | 379 ++++ norp/makefiles/Makefile.common | 54 + norp/makefiles/Makefile.linux | 27 + norp/makefiles/Makefile.macosx | 48 + norp/norm | 1 + norp/src/common/norp.cpp | 2704 ++++++++++++++++++++++++++++ norp/src/common/norpApp.cpp | 378 ++++ norp/src/common/protoPktSOCKS.cpp | 388 ++++ 17 files changed, 5282 insertions(+) create mode 100644 norp/TODO.txt create mode 100644 norp/VERSION.TXT create mode 100644 norp/doc/norpUserGuide.html create mode 100644 norp/doc/norpUserGuide.pdf create mode 100644 norp/doc/norpUserGuide.xml create mode 100644 norp/doc/resources/ProteanLogo.png create mode 100644 norp/doc/resources/norpConcept.png create mode 100644 norp/include/norp.h create mode 100644 norp/include/norpVersion.h create mode 100644 norp/include/protoPktSOCKS.h create mode 100644 norp/makefiles/Makefile.common create mode 100644 norp/makefiles/Makefile.linux create mode 100644 norp/makefiles/Makefile.macosx create mode 120000 norp/norm create mode 100644 norp/src/common/norp.cpp create mode 100644 norp/src/common/norpApp.cpp create mode 100644 norp/src/common/protoPktSOCKS.cpp diff --git a/norp/TODO.txt b/norp/TODO.txt new file mode 100644 index 0000000..2ec0200 --- /dev/null +++ b/norp/TODO.txt @@ -0,0 +1,37 @@ + + +The basic NORP server state machine is in place and pretty much ready for testing. There are some details that need to be wrapped up: + +1) Implement ACK-based flow control using double-buffer markers, explict positive acknowledgement, (DONE) 1a) Add option to NORM to +only ACK after application has read stream data. (DONE) + +2) Clean up debugging messages. (SOME) + +3) Provide NORM congestion control options (DONE - CCE added) + +4) Session configuration options (ouch!) 4a) Src/Dst based proxy methods and parameters + +5) Implement auto handoff to TCP direct connnect on port unreachable (what about NORP request timeout, too?) (both DONE) + +6) Use protolibd for "safer" socket state + +7) norm_rx_stream can hangup shutdown - fix it (DONE) + +8) Win32 build and test + +8a) Win32 NORM-CCE support that's not WinPcap dependent + +9) Send / handle negative SOCKS replies + + +10) IMPORTANT: Detect remote "norp" failure _or_ make sure that we can _always_ detect when our local TCP socket (originator client, or +correspondent remote) closes. Right now, we stop TCP socket input notification upon NORM flow control block condition (e.g. failed +watermark) ... we probably should have some failed watermark timeout condition, perhaps in conjunction with using an +application-defined command as a "keep-alive" mechanism. For example, have each NORM sender send a "keep-alive" heart beat +periodically, but upon multiple periods of failed watermark ACK _and_ missing "keep-alive", initiate session teardown? Otherwise, this +creates a condition of persistent requests of ACK indefinitely. + + +11) To provide easier firewall traversal, should we split the "correspondent" norm tx & rx sessions so that it can "talk back" to the +originator source port. Can also use the NORP messaging to signal the norm ports to use. + diff --git a/norp/VERSION.TXT b/norp/VERSION.TXT new file mode 100644 index 0000000..ba836ac --- /dev/null +++ b/norp/VERSION.TXT @@ -0,0 +1,29 @@ +NORP Version History + +Version 0.1b3 +============= + - Added "correspondent " command to allow 'routing' + of proxied connections through specified remote 'norp' peer + instead of directly to connection destination address. + - Includes fix to NORM source code of receiver rate estimation + measured during 'slow start' phase of congestion control + +Version 0.1b2 +============= + - Adopted NORM_SYNC_STREAM policy to guarantee synchronization + of stream start at byte offset zero of proxied TCP stream data + - Added norp "persist" command-line option to allow control of how + persistently norp tries to deliver TCP data in disruptive network + environments. A -1 persistence value is infinitely persistent. + The default persistence is 120 seconds (2 minutes). + +Version 0.1b1 +============= + - Fixed bugs with "forward" command recently added + - Updated "norm" with bug fix in normSession.cpp + +Version 0.1 +============= + - Started versioning + - This version adds the "forward" command for non-SOCKS proxying + diff --git a/norp/doc/norpUserGuide.html b/norp/doc/norpUserGuide.html new file mode 100644 index 0000000..5388c85 --- /dev/null +++ b/norp/doc/norpUserGuide.html @@ -0,0 +1,23 @@ + + + norp User's Guide

norp User's Guide

Abstract

The NRL Nack-Oriented Proxy (norp) project includes software for an RFC 1928 SOCKS5-compatible proxy server daemon that is able to use the RFC 5740 Nack-Oriented Reliable Multicast (NORM) transport protocol for efficient and robust data transfer between norp proxy instances. The norp proxy automatically supports conventional SOCKS TCP proxy operation when a remote norp peer is unavailable. This software was developed by the Naval Research Laboratory (NRL) PROTocol Engineering Advanced Networking Research Group. The NRL reference implementation of NORM used here is available from http://www.nrl.navy.mil/itd/ncs/products/norm.


1. Overview

The norp application ....

2. Theory of Operation

The norp program acts as a SOCKS proxy server. It supports the SOCKS5 "CONNECT", "BIND" and "UDP-ASSOCIATE" proxy methods for conventional SOCKS proxy operation. The current norp implementation does not require (or support) any client authentication. Future versions may provide authentication or other access control mechanisms. The current norp implementation only provides NORM transport for the SOCKS TCP "CONNECT" requests. SOCKS "UDP-ASSOCIATE" over NORM will be supported in a future version.

Unlike a conventional SOCKS server, it is expected that the norp daemon can be installed and run as a local "loopback" server that is co-resident on the host running applications that wish to take advantage of NORM transport benefits. The norp daemon implements its own signaling protocol that will automatically determine, upon TCP (or UDP) connection establishment, if a remote destination is also similarly "norp-enabled" and establish a NORM transport connection as the proxy connection. Otherwise a "business-as-usual" TCP (or UDP) connection is established on the application's behalf and thus compatibility with "non-norp" hosts is also supported. Figure 1, “NORP Concept of Operation” illustrates this high level concept of operation.

Figure 1. NORP Concept of Operation

NORP Concept of Operation


TBD - provide some more details on norp signaling for peer detection and NORM session establishment

Note that as an alternative to making proxied connections directly to connection destination addresses as illustrated above, a remote norp peer "correspondent" can be specified as part of the forward command or, for SOCKS connections, with the correspondent command (see command descriptions below). Future versions of norp will include more sophisticated "routing" options for different destinations and traffic types.

2.1. SOCKS Loopback and Intermediate System

As noted above the principal use case for norp is to act as a local, "loopback" SOCKS server that can be used in conjunction with a properly configured SOCKS client. In this way, all of the configuration parameters are localized and implicit and no precoordinated configuration with norp peers (or non-norp hosts) is required other than using a common UDP port number for NORP signaling.

However, there may be use cases where it may be desirable to deploy norp on intermediate systems at the connection originating site (or domain) and/or the destination site(s) (or domain(s)). This is easily supported by the norp design and future norp versions will provide configuration options for this type of deployment.

2.2. NORM Protocol Usage

TBD - describe how the NORM streaming capability is used in a flow-controlled, positively-acknowledged fashion to provide a reliable TCP proxy function. Also describe the NORM congestion control options here.

3. Usage

Typically, norp can be run in its default configuration with no command-line options required. However, a number of options are available via the command-line. This is a summary of norp usage:

norp [interface <ifaceName>][address <publicAddr>][sport <socksPort>][port <norpPort>]
+     [norm {on|off}][id <normId>][nport <normPort>][cce | ccl | rate <bits/sec>]
+     [limit <bits/sec>][persist <seconds>][correspondent <remoteNorpAddr>]
+     [forward <tcpPort>,<destAddr>/<destPort>[,<remoteNorpAddr>]][version][debug <level>]
+     [trace][dlog <debugLog>][lport <localNorpPort>][rport <remoteNorpPort>]

The norp program command-line options include ...

Table 1. norp Command-line Options

interface <interfaceName>The given <interfaceName> specifies the name (or IP address) of the host network interface norp uses as its "public" proxy address. Currently a single interface may be designated for an instance of norp. Future version of norp may allow for multiple interfaces to be designated depending upon the source and/or destination address of SOCKS proxy connections.
address <publicAddr>This is similar to the "interface" command, but allows a specific address to be set. For example, hosts with multiple addresses assigned may wish to use a specific address for proxy functions.
sport <socksPort>This command is used to specify the port number on which the norp server listens for SOCKS client connections. The default port is currently port number 7000.
port <norpPort>This command is used to specify the UDP port number used for norp session setup signaling. The default norp UDP signaling port is 7001. The configured norp port number (and NORM port number) MUST be unblocked by any network firewalls between norp peers. The given port number is used by norp to listen for remote connection request and is used as the destination port to signal remote norp peers.
norm {on | off}By default, norp attempts to signal the SOCKS connection endpoint to setup a NORM transport connection to handle reliable data transfer for the TCP connection being instantiated. This command with the "off" argument will disable this function and norp will act as a conventional SOCKS proxy server.
id <normId>By default, norp will attempt to self-configure a NORM protocol node identifier using the IP address of the server host. This command allows a specific NORM node identifier value to be set. It is generally not necessary to explicitly set this value for norp unicast proxy connections.
nport <normPort>This command can be used to specify a UDP port number that will be used for NORM protocol transport connections. The default NORM port number used by norp is 7002. The configured NORM port number (and norp UDP signaling port number) MUST be unblocked by any network firewalls between norp peers.
cceThis option enables NORM-CCE congestion control operation that uses Explicit Congestion Notification (ECN) information for NORM protocol end-to-end transmission rate adaption. This is an alternative to the TCP-friendly congestion control mechanism used for NORM by default. Routers in the path of the norp peers using the NORM-CCE option MUST be configured for ECN packet marking in response to congestion.
cclThis option enables experimental NORM-CCL ("Loss Tolerant") congestion control operation that uses some simple heuristics to try to differentiate packet loss due to congestion versus duo to channel bit errors. This is another alternative to the TCP-friendly congestion control mechanism used for NORM by default. No special intermediate system configuration is required, and while more loss tolerant than the default TCP-friendly behavior, is not as effective as the NORM-CCE mode of operation.
rate <bits/sec>This option causes norp to use a preset and fixed transmission rate for each proxied data flow (e.g. TCP connection). This should only be used when the network connectivity usage is carefully pre-planned and previsioned for the expected (i.e. a priori known) flows. At this time, one common transmission rate is used for all flows.
limit <bits/sec>This option sets a limit for the cumulative transmit rate for all flows that norp is proxying. For automated congestion operation, this can also work to "jump start" the usual "slow start" transport rate control by setting the lower bound of rate adjustment based on the limit <bits/sec> / <numFlows>. For example, a single flow will immediately "jump" to close the full limit rate, while the second of two flows would "jump" to half of the "limit" rate. Also, by setting a limit based on a priori connectivity information, this can avoid rate adjustment "overshoot" and help congestion control operate more effectively as compared to a "blind" situation. IMPORTANT: This options should only be applied when the connectivity path is well known and the impact of the lower bound enforcement here will not adversely impact other network traffic flows. A future option may be provided to further reduce or eliminate the lower bound enforcement that would eliminate this concern in less controlled network deployments. A limit value of "-1.0" (default) disables the limit enforcement.
correspondent <remoteNorpAddr>This option causes norp to "route" connections through a norp peer at the specified <remoteNorpAddr>. This is an alternative to the default behavior where norp attempts to connect directly to the connection destination addresses.
persist <seconds>This option controls how persistently norp attempts to deliver data to the remote endpoint when the remote endpoint fails to acknowledge reception. A persist value of -1 makes norp infinitely persistent and the corresponding norp session remains in place until all data is delivered. If not, an orphaned session will remain in place if the remote endpoint is permanently disconnected. The default persist value is 120 seconds (2 minutes).
forward <tcpPort>,<dstAddr>/<dstPort>[,<norpAddr>]This command sets up a "preset" TCP proxy (non-SOCKS) port forwarding session by listening on the specified TCP <tcpPort> for connections and then connecting to the given remote <dstAddr>/<dstPort>. Optionally, a separate remote <norpAddr> may be given. Otherwise, a norp proxy connection is attempted to the given <dstAddr> platform on the norp <port> (or <rport> if specified). Note that multiple such "preset" proxy sessions may be specified on the command-line and each "preset" proxy session can handle multiple connections as needed.
debug <debugLevel>
This command can be used to control the verbosity of norp debug logging output. Generally, the range of the value is 0-12. A higher value results in more verbose, detailed debug output.
traceThis command enables NORM send and receive packet trace logging.
dlog <fileName>
This command can be used to direct norp debug logging output to a given file. The default norp debug logging is to STDERR.
lport <localNorpPort>This command can enable single host, loopback testing by a having norp listen on a different port number than which it uses as the destination port for remote norp peer signaling. E.g., two norp instances on a single machine can be set up with unique <localNorpPort> values and then use the "rport" command to specify each other's destination norp port numbers.
rport <remoteNorpPort>This command is intended to be used in conjunction with the "lport" command to allow separate specification of the destination port number used for remote norp peer signaling.

3.1. SOCKS Client Configuration

TBD - provide overview and examples (for specific SOCKS clients of note such as Dante, Proxifier, etc) of SOCKS client configuration

3.2. Usage Examples

The SOCKS client(s) must be configured to use the norp server unless a preset TCP port forward is specified. For example the Dante proxy distribution (available from http://www.inet.no/dante/) has a socksify command that is installed and can be used to launch existing network applications so their socket communications are directed through the configured server. With Dante, a SOCKS configuration file (typically /etc/socks.conf) or the SOCKS5_SERVER environment variable can be used to set the server address and port number.

The norp "server" is a lightweight module and can be installed on the same end systems requiring the performance benefits of NORM transport. In this case the SOCKS client server configuration is the loopback address and norp SOCKS port number (i.e. 127.0.0.1:7000). The locally installed norp SOCKS server will signal remote network destinations (e.g., upon TCP connection initiation) to determine if the destination is norp-capable. If possible, it will establish a NORM-connection to the remote norp correspondent that connects to the final destination. Otherwise a direct TCP connection (or UDP relay) will be made to the remote destination.

4. The "NORP" UDP Signaling Message Format

The norp proxy uses UDP signaling to confirm presence of a remote norp peer and to set up (and tear down) NORM transport protocol sessions to support the proxied TCP (and eventually UDP) transport connections. The norp instance originating a SOCKS session request is referred to here as the "originator" and the remote norp peer to which the request is directed is referred to as the "correspondent". The norp "originator" is the server associated with the SOCKS client making a request while the "correspondent" establishes connections with the remote SOCKS destination.

The following UDP payload format is used for NORP signaling:

0                   1                   2                   3 
+0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|   msgType     |    reserved   |           sessionId           |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|                          normNodeId                           |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|          normSrcPort          |           normDstPort         |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|                         timestamp_sec                         |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|                         timestamp_usec                        |
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+|                           content ...
++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+

The NORP message types include:

SOCKS_REQ

The message content contains a SOCKS5 Request message from the "originator" to the "correspondent" norp server.

ACK_REQ

The message is used to acknowledge receipt of a SOCKS_REQ message. There is no "content"

SOCKS_REP

The message content contains a SOCKS5 Reply message from the "correspondent" norp server.

ACK_REP

The message is used to acknoledge receipt of a SOCKS_REP message. There is no "content".

ORIG_END

This message indicates the "originator" norp server is terminating the given session. There is no "content".

CORR_END

This message indicates the "correspondent" norp server is terminating the given session. There is no "content".

ACK_END

This message is used to acknowledge receipt of either an ORIG_END or CORR_END message. There is no "content".

TBD - describe NORP signaling and the message format given here.

5. Future Plans

There are a number of additional features and refinements planned for the norp implementation. Some of these include:

  1. Source / destination configuration and "routing" options

  2. Data compression options

  3. Security features

\ No newline at end of file diff --git a/norp/doc/norpUserGuide.pdf b/norp/doc/norpUserGuide.pdf new file mode 100644 index 0000000000000000000000000000000000000000..05e2b7d9c24f259741d291a3754c4c2cf56521aa GIT binary patch literal 126544 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 z zqf~afsTPglYOlIUOL%)^E_-xnc-i)*pDS`8(%z1OPh!g1UwQya*!7H^^n5R*618CO7MK^p zPj<1+2C!=%;z1}B&pn^kxvo(%Q@DF(N=hIR z51y<9c`urD4sl6}%PLIh7`Qk^2+%H|2*4Ov4#PKdMrMY#EaSLRRcs`ps^7&S2y6z; zf6uH4Sqg=~XpOv*jlx1l`pHf?QLfTm_#Yt;G528`;QD7&xauEzPUvj-)mB2- zy!RwzHm;@3Dsl+E4|s$$-CU1)o28Td_4}BkP_POph*ajd&<3#duznm-$?i{F&y2gk zz0nfEH928n?ichL3A!3sNxmkM&XD!c(gEyaDMdFnQHu zZ@EQ67Y=dETq{);WLNA|f33i%ikKo`Xz6)kXugBW-PF8~+QjHHj`cZx9^`?9MMkD^ zS}5mZl0hrD^^1xQ%1${r1VCip-sQFWjR5bpw@Ko0dcm-4cwv(CbWn&RJwBduUXI2B z@8*&m);<5*x9U@iN8i%1vo#K@H-9B-*zYIY4-L?ywd?}D4#4%VcqM^zdKPknvy=58 zsbIB)q(;aymMHaJoa}uUu@@s_Q~?l(zVdV!hPPmy_fobyH&<7I@t8@<6w}^;7{i#F zJmw)f^Vi)N286Sh(*i8W9)u$3BIv`E2u*aDM(2W}nnzb#aT$5DT?i?gVwM?gfdGj( zrEkR|!?$WX6AhgFNL?}p=Y!kk6z`mmb4>i_^b4G>{M4lxy7+qITH#vODbkt`QmQ8mfdGvF>rsFmwz8&bcc$6Q;G(TnYd$s<>CP9#(=)3w*O@HJGKH(;M{iD@tDJapU0kYBG<45 zM__Y3jSUC@Q&KNfCM9#1X#O14qTcWG$&O zE{UZy)DEAsjdT?SN{3yH>#aCTA1HOtKDeg1Ax~W3cL3Ho2n*~k(f;j%D_#(+N;748^`9m zH(7gl6IG}PO7X5XZeTlXpb*2shr+8_yrQxulyhKCKdzF(MwMi91vR}LCn_(7&t-KG zFcr8}01x$)Su#cy?Ti4ICILi>*iDui#l#QZ7NNl&0Rp_c?$lxrfmg-zB=f-EocQG# zfw2Acw~v&oS1i>^-qhT+T`D|gKZ>LK9&HaaC&sZ|&ZcO|Q-YY0PwWM-;zpydh^x$d zo6c-@)WYuT*g_O}kG)ky9ea~2MmNMS*3KxEq8c*-`LE8Q-0ssL&K>7VZZ0m{pVxz=UO3a43PO3tWBlbO!WA;rjQuN?Et`skpKw7 z9>n&r*>}ums;Wlkc4b=g-lkFGR&o|v!veOwf`1Aqab-5~ULVx8ZAGPwc!@Av&FH$zXr!CvcWk3Q z6d-GAW_I(c_b#`}N1);cy<&b;MR4mDg@6maF{4)y)A`o`{tBeYXd1=Axq7BvJaJ3F zzE70%M%G;FW|(?y65hB6&h$;r46gVJvex{#odo{h&n5lo(32CnHbnEABc&;-nb`s; z>V{_rYc;qS0hl1R`Ht*!ITaONVoZ=)hO52NT9M(6q*l3}lSg#r$97nL<0;CUjrV@b zD`~Mhu}4}tq^fV#thh*vwaCB=A3s~GFfmnHD$XRxs_eqHk}o#U!+-z5m`CQ?PLD!j zZryD#1wOXsx8JV$v65!YdRZ9>TvT?~G5LeDv&xO<@W;&tS8&A#rX8Bo0Af6V6?m4D zkHAZbMRmoah{329DphNtn)#q+#y+W6Dsaf2w-Ly5pJovnwtDw%u)Oeu&-@CcvsqhPr-RwCv1vPf z2Q2^z;v0^J=LLdYRaJ)oI`KJ0I40J*o+)w2A|J3|wF+skK^`7RA|{QLeyhenXiXTO zgR5q8no`-uRLQ4`fKD@i*Bk5MBZc!|45d(&$L~c>E<@>9eAL`4$E`W5-3$`F$=S!e z*?=h-pxBKypH2L_^}qpv~GOO8fVOU~@xo)ZY~OiGgBO@g+7vTw5>gW$cD~(A>2DE$=*BhfzMjb%A933SG^nd5h;_3H{ zuFk1IBQTePc&r;w=PtM0W9Rkg(qka5Ey;xfzvC+2n{D~q4x4C@vUOABJT-=ICJE@|gAfUJOb7{pT{uB$8S|9sio+sAAT z1C0b&gs&CvYJ{V4gac0<0HOzpD&1fxs+hAe{Nr#TNMbQQk+TJ=xpag9IH=SI&@PsA zm**cD?pKpd1#-1Z@F7Ba>AB?7bV<6(*0nXM+e<0-aHS&yTD47(jE+U0)r?mT7^=@99m08b_m;MLNY-El<8S z`JPjFt!9d3kH!A9&yvU-8itI^zz_k%4$0}4L(fHf=w;rMF8eFAfz?j22hWYk5BU(E z-mjB#+{;mi%~00_Ohh%z$34_mCvNM^$0G@eRHb!_5YHYhix6LjZtd$xLvB0>2iXvL zDj6c68WAANRDNdwMZ#Wrr{cjAc-_sdEx6g7Cgka4T0?jT`>?;Wqa*URAW!$BT;((Y zfMLjK(Z3@U4d_vt2s}S{2pWiQ2k0>sb|j4+-Are(_oS!;zk3$kUYlb{Qn0RH+>*kJ zbt$C}hv@QJfrya!+D{TmDHV0H&TQ-(Eu|60wqLoTH1Bq)+<~uki_1D)xV%*LV4OGV zdUgX9WLICcQ(isD+aLW%kkV()@%pp5v#&2aYW|Du_?inyh6HJtUPoUnd}jkNQSRAq z0hJ8|?27Jx<^)S*fb+^N23{84N}f(iNjZ2S?X{&1 zH&@p*QGXHeVqlQFdb)>NyWcPP*!NI#?XWf3rq;pK;TB0aFG38|U%`M9X|Gb92(PcBL`dhBQL3{$qO+^dD%r@#Wr`C{ATbGkSB zPTh_tWdK1SjRhEJmRz~Hxd6(BBvSf^7rs^#5Qrf6;`_?V2PTEfM}TYrU~z%_l%BKm z{eG@hD*n32e(!tGy>JaTl;gGNNv5M;u_Bw zgoBEi6Q~O`FvuH$;Xv&}?P6=DLor0iSZq)Xh%2Bay5aY=FgCBL=?b2R1;EEOv}79I z6%|Fic#-WrUZ_zZxZU6k#GD8QlgQDJ+L3}X64jF?A4&UDf7O_ zURJz?-Jfgrs7Gjl=^(p7E=t|8UCD>%=BsdsO8lnlUuq$DSwJ(5%$;acWk zfBMC#54EubNI}LE*HDFOeK=F_k6OE0S*?NCWH61WtEu6QSIz<_JIIO+aSg+b4z0)a zs?2toFTO3@HZF2IW?07aFhYJ%w!hb=5VCDdiqO)dhYX>!B6vpWgJNnjt>ZO+do*`& zkcWzf-;cDHURxSy=4nkx$+T;`x;}nC@LzNovk{-!T%gGie7MCkkUj>m1LEuL8#}Ic z=ep7b$S@X;&nbjF2D4=N5+ii}h z7cVxPowh42Z$b|Xj;xM=B&E^32>~<==pC@jhG2JO4aE zt_FhrVr0P$e*i+_;^G2mp+O~;GE!0F*^ji*DI?$x_gMvHabYXk18cRD0mjk5ag ztW76b$8YI*v<@B;QHz=M?U}YZzM0rKf0U=P*iG@^m@d-Hf2S3dLS%vp15Ub#0N@{x zc5h$b3`nJT6?edVt9O2y^v<(g9lmdy?m-5~47HfTQ%}7sR(7RD-W1sLX|YMM01O_a~&YEM;H!T~rkded{YM zNKSrrR|=-1xg>P}efY&zBRf8w%LX3O+Qq>;{gJ%%>%GFDWCy(4{iXTP@-KRi9&Kz} zYmhR2LdO|~C;fp~13f1>oiHk{r!Vt@Oz)AhR^rEf&_NBqAuJGI;MFL? z$Y=u!fwa>;LPigPu-oj=QVJ8~EIm!wko8v^emEy{j~ZW6HQ@u5ufLVlT`G5$v(E!JbZ#VzoL3E! zY&D&4wPXQFg~yfS4Q!3hPHBg?^c}hWN97qe4Q%C3NF5+Ay!GuWT|7MAYr2@}OIAM&t$=vo_Y&fD6mP)E^^AbQ=qGOP5^|?T1&;#!SrB z&aK$lE7=VHUMbVjNJ*Dx2`IhcLx=uLjG)&V*7tUv@eH$s813UV63TqS=<4zG(`WjEbmf##$Qp}Aq; zcWM?r^&Kv;RzAOm433ZAwGxz@`m|4QL-s!Qs-G8ylA9%-sh*x5TnRLnaA?t;m7X>) zX9kEHAfIFuCF9v4&=C+@Ko)+$V|qk5b6RGNtbx)XYO1QSiHVgU1q-;y88r5ki?w>E zUr|Zi^k>GtWRTs-z9S4RTRu^LIon0C|2#{V`EK`sf_b;n=3QX~5z-YDh;EG%8t$`g zhy8A`Z_MmBXmpOk;$DzsZ}xD;i+)H*K=(_?S~^dNew{e3e#~aCMhD>||B;A78LNi~ z38IP!C>6RB9;sAn_riNc@rfKu?D5?UzFF0igSQUYT@U?Y3E~X7W?HwS1h--)EMy1c zW_yaw!6%>7O zXxQz9=kaQnA`i$+g<`A%;4>~8FA+t7Cs&xF)0g4~0Cf9B7dk?~xzt~^|` zc4gWudNqz+;@N0IzS4ADH~kb+0Q#8w*$+*0gm{k2-GS!X=Hnf;YrEY|z6R#v!(5%s zjEU7I5kB*n{*D%0qj!k9TQT);B?}bs18ar~g4rjd24Ywk5%O=(fRZ>rj9n^AzqsJP(7k1yrlLAH9KPaH7v(uBQV_Ty-WHy%ydP-t>6FfdT0)F4^$*vT?< z^Z^c$lMC}Pk9PDo?4gi)-h5q!ixeiLRe0FPJIBNd3`U<*kV}Jt9}O@jK<-BCT9&bLpR%d-VJ19O+yx((#^`QdeoQG$pQMo9d&rV@|bQ0H($qmKJ;-G%^7 z8)UGMEYTd+dsoGa0izX@3fR*iZF#EFhSzS2%YExBfNKv32msmc2#Uc&--jgT8_$Kl zB8Zs16}`>id$7X*O%IC}37DV#6gO*AJ*|=^psACAYOkQKid%4hCG$FJ-F{B|a zM@&Q!`a<1OkZQY{tJKaZ1WhjN{ivKily+lRIVRtELf^gb75kl&Pft!M_$8olV`>Q` zqX4)z@B$il3Xk2Vfq?-)EftokZf0V7N2lp~vH2K<^ zhr4om7VZ(fBmpAHvX-%kgT7i(5)Rqa(WZk`h(Mr|)Bz|r z@cLp~1$YF2Runy|RtCwFhi9kz-^n7S16y%mHRP0(4rSWZmP{Oa=;noqAP@L}a68w; zZR=~T{Y)M3)_JWqLx!y7HhY5abPGjMV99QK3uDH=P}p(G1_-w8ha98cIbGg6T@F{Z z781D$o~O@b45HDp_*@H4x=lWxpr?x_?0=tk@DF^E#3xO~0X5)2nF*JG>SBXH4<3*p zZFq*ttXIQkT!oCi0`z0YHd79r;_<1eDFB}UvdJKQPISttuj1qPGp0=@h zAgTg;=@LmWk-dD!McV-kJG-4~eE#0CukdA8i`@B01Erd32Q6O5=Ua^`4g*bs-E>h8P71G@~0} z4KzX#6XgbY+7jV#e>zBc3|u1=IJrDSq;C)@IkI`B!Y#-5)<<%nXLbM*w_@l?l9QYdo_+rrBFqrxctDRM-9w)M@>WvgO65U@s*$(3 z#OvolBycV>?)m(=sGvan?Q=|Ifa9g3rp6)9L%E*uEdd1s@ws?M`YSr889O`UnPX#D z!X3n3$FG&zoQ1F;TFe{wAKamiMcBx}G5zSY-FU3^5JX%+kr3j0Ov|)ER&G zaDu2XyGkoABSTcJiQm4|>|}$ws_a6oC279F#}N00avs3RMet&wmPOKT_PdOuRba!& zQb999Ga^y$q34R=2+_3Gk-}_$=Z%N2dWTvSai0L!lm_mNNE38Nnf!z-)+P_aja^+` zfq1F7NrHtVPK@$AfQ|eq+I?W};wZ{VW?=vax+zV3Qqn^$Es%^CqZ&SD4QlKvSy@?u zBSdh1CU+R+Xmi%##nS&AA`X;5v62ZB2x%Df^z@XLmKLadQz4j)Xti6|0g`0!C*GEc zuxk0}S%d&s&hrP)4Os8e(;$-!1sN_?7C^~k;8Xg%;3zqbV@e=4fd);gT@r zgWd#XPi&Nzmsi`*klIPa3?b&yQFqHC!l};ZC74Y>YC8PfP^MeGz~HX7{SvSihQ`5h zA1o{1$v8N032c1;;7`GkxKTZ(pMY{*W`gRb0-h(_UL+y>m5a{|7US-`{*`hHl9)jv zxaD*JOtu?-mTbhjWtKX^Shg~dLavm;qp78}?o#*jcY65Zb#^eo4*|*8Ak&8Cql=r{ z)>I{99Z%8Y=BBzjuj8#5>q>#TmoPLuC%Cz8Nd*A0IXRty&ID3_(P>M-914s*qT(^I z@@Zs51p!Nbw7NW?xfZ-Ry)jUofe}^p#TMS21UO@crY7nO3hn_{BdFci(FE=_U?Iy6 z5&(;pD476;HqotHpjN}3sVW#%3s_B7%-fLnzcRI|=fv#x*{GN}1 ze@ojx>Sx}nEpTH1wBJdVCzfpt++bn`*%&1E9OaehEie}JK)xE$&xu*i&(9A|t#)E4 zvOw_$rrq$+5c^-J=|L7%ko69z0E`~U-Ur@Z!y?aaj@>K&s4DdC=fTgji0#O5fHeZZ z(~3$;SQ!|?s;H<8X@nJGfIl!HAz{!7eoJ=KRb%7h<`x#Lx)t<-g5E6#(kXpfs;Xt6 z^jUZ~G_bW4xPrhVvQ6M4Mx#1_fd!GFN+hv>Jg_Em0D)7ZHON54fI?TXSlW}obOVSb zvY>z9Zv4B_(w)IfiM@EI*gP;D0laT;l-w^Eqr5(p>rduz1Oa**SN;{0pw3z~v zQl67*gm?c8ErCHGJSohgqRnsKfZFG*Q&pHTU=RSQf%Rk)jm z^~B!-ctBO-bKO6Q9>9`GV}sScc9XqTOH zR6ZouZ>PR-n2vR<>k5*lalv91>^a@77qq;*yn=%D05nkcFb$qe3N3P*^rm zQtIng)^_0|2PbrBzT|mHhs~N3ehY~t3cenMqXIIcm6Vjk#l@wirJruyi~VTJV_|Aa z4mcw>xAPI*+Tj_%{RD8b`IEQU;Vzm751w*7nPktosttx+A_FcAIO(wE0c;DfTYx07 z+&r==|H5NC`9M?i@Mzir)spfiWMiTv>59VBcBZZp2o->u2c*FoFWi6t@IP60$N`u^ z0QV706F5280Boers2zngB5Eo3HcyHj5H-L>_o&i(3}8eCJoyY(?+6JA&CJb1=IWjs z^Q7$W?*p@SP%s!K8`KIG^kBZFKnT^<)d50@h=>3^NBfxFXTA@vw9=o30SU#rtqWX% zG0=Av6)t_r++c~|Gm139Pgsq|y1L;UxMDHgfg)0XIl#(DLSmxbY{M7x@?zae;K5<7 zAf(wlwQ}8utmLJf15p|>Sh^Yq@HoIMwNO7p4U&FIe-Z*&`IiQ(5#Sr+$R4TzKLUlk zH9_Z9ADqpB>ItA#fF=s8Pa#JU;|zG+%%-NMoE%fOcuZXB;c}4v{gdFkTiz8H1Dt#d zDXHKwYk;x~L=K2>0Dr;j>gzL}ls;3A*L^EzXJ=DWy5(PrM%IA6N5rg~0G9Xewg2Mi z5bwFxUktrD+eo(9j}{<_0qw8T-ezPCz2AQG)T8c6H_U!pB7-fx-vhlt%r(@}0w%56I5{Y86!e zCzqWoq>)qekw*c}2g3~r6+W+_4a4U~F=-Ltw*eW@g1e!dO>IJGp|%&hPRJENcNrJqWaVqX?eM zPgNyG^%F2W*Vln88wOa+K<&DN6AdT;EY+pJkrTlRp5@{Cn8ZDOJTaV)WMIV^jbrKw zA359@`#A*zTO9!3VA%EAoL*Gakvy^n4fNnD0@`rk7u#8I-#R+4hb;9#nL;1*alJ$s4G#=%z@y!4VPJBLjON#)94(WKpd_=iQzPB7i*P&vE_IT@;H_5 z0%dfsdI%_s3ShCEG!jrdSXKm;E`eIsHVz+=)N9JS&!m*i>`k3roXm{upkT|v*c!^i z1!IH3H@NxPIrx9?<>LHfuaFR{hNq($tF(i?3zSve)z}684Ze#V$|`N;*@|ar@WNKmS7EXa9Ab ze?QBA{_^XH-|c=K_xEqV`}(glviet?OCt%E&yP%u_oGiPTg54gAmpivjlb~am%-jXa4-Wq0yA!+W3>bQ2e%9!1(cng3&sj=6g(zySFSKtHyEosl$`@y+Y@~J zaY-|H2f(QT!~E<5c!Hn<`hOiC_@MrEeEvS@zlcED$PRSs_u#UMIh&Z-yTEt=_p-w2 z1OA1XotqcRDq-X(`|Ck-^8f~wwl%VFhH-NKI*Yitg9q#pGw2&L2R9pxmz^8N$Hol^ zPtL{2*2+Z8-on-l#s+xa#mr6%#tqmVz9su}OZ$)0a&hqdwexWL`=j%UX7&~?mM|W^ zUnD7QWeY|VjGZkQRY@~Axxx?pLlk*T>BoyjrQFAnI!?_hf6k@D*ZDx7`;N{_=_u)4 z>K@Bfa)>)GmKrA7Vz2|H2&GaElPyKS`|AQp--{hDJ>jmc1g+)oUmM@rxRZ(NZZ>QB zR?Sj2kx!YsY)oWgbaBvZDL9JS_Txxq4RKXYd1 z<#9W--uo54sM!XkgrdYq`b>Mt<&!$m*|RH^x*Rtb&#LL#SL^Qvm-ZU#XsZ399@m#9 z8(bdGe{WQI|7x4p1x;UZq6uo1Kjr+X%CpINIw?v2<r+!rG`c6IR2fJqv_kHRV zeW){9o{_i2lDCGXws6;`*O;c^fij<-H(0unYiN=TsW2wvg|&O2JRhD>_C(Lq?cvu@ zKQt6ba%5T4X;WXlvx6&uMjnKPh?*JLBDM;1#4$(z5VVi`S>_dOH=cP zhsRRQ@~FXqKR+phqd&MyZOiipr@x0Jo$3|-U9J5>M7V%9d zA=XtNI>c*~YJr+!Qf9kOufX>-qT)c37`PV-VPWYaG{R1t2GCnsq5A5IH|7!Ke*3Xu_9B>MO z|AoJKfSSVtdd&OtO24l6b3fR}$H@tA1BL$vZ};n{Kaco%o`1W>pM8O!6W*WSpTH^d za~uBs*Hz%Z!ngSN_~E}m;lIG&Y%or+2mb5tW8mBHKEOZ0`>%ouXl4IRPyPA8{w}Eg zP-y=YRI+BaZe}i4CPvKS4z{L$(I|eN@~^}94|3{X>MAGD!=AW0xR{w5+d?^k0Q&jG z8R*frK$isy;V-on3OF9FtO9lIzbdQ$Rquqc%KV$^31e0Iuc{}ERr4S9NLHXM|F6_Y z7^?@A)$?x>^S90RTZ{i^iTQ7j_P4;~W#{{gvdF>qH)ZkXkH3^f4jvA6UNDLNMQE}E zdCknh!3Se!W8;MJ^Kkr=%E-?3hxRB7V~1bvx6;TBjKzOhBtVP*lSBVd8hQV)Z2o7Z zu{C^}jX>)73hJu*ibYMZ>VOLYpI9x#e8`Qsn$l3gsgtcN@*C6M@z~)_^;PO2c0XQz zqI3~|FDo7uzxB4R!D5uCj~|by}x3UYBRzk;8~ZVIS`LPGh6MMpzQzic-VrRN^Jcr`zHn99!Y6a%UA3R=c=IANs|5Jkhwtl#{+5VK^aH;lJ5dbH} z|BD1?X9raG7Y&f>FCzRuMBCrQ*B{pN-+2l4Qx^YSeEp%2{e$@WPi>3$FLu#SXUacF zG0^pYwksCKTfG0t{D0Az{vX-@FPi8d><=`_ zU-QWSq%}QgRR(GPjo%qRBUq8kmj}$NX(or?^@8LQioldoS%#A(t-My}7IVj9_-OoI zUZ!XG`ny|~2Rhw0SBWO?=c{9o{k)F7pku)65Xp_Zr|$Tn18qv1u!>@}l=tN(#sle2 zW`$;Q9w)(Q+|W+zu1|P-oP=G(=96#3u$iu*(7-a`AAajE_uTfb!f#BWrD_t=kJ3o~ zpYE;%E~={SS7z2|V&#(JGGV;%YR=qs76l9uG!Z4iJ=BpI6oHv>7z~$ON-I&5+{z%e z()L#kw_Gt5mo&HBO0z7?N|ZD=Qt^A9b7wjCUU1;|etp0C>^IzdpL_OaIsfN5=iG)w zl}yfWb7bS(=4&dOY#A{-ap{EOarwQ@{4#BaHgoo;rJG&^y!7~>t1GT;;yQNFhpfC@ z_oKPlnyFXw#54UDF?F|<E%Gin_2%^lS9cpW zLOTL~CHwOVo7@b){A}5KySsfhJEToX)RuSZEbhJFwMDH$jl`LS-s{G;b)D^rNS$F z?ds_UZ*I-!gc)nMe}4O(!E=&gQiDqdmX?2VKKT8EvG2CM;?7=lIcj2Vmf-++Wog&f zGmbM{%6s(-wyztr|MK=rdq=lyI=6V|9?`q!R-03<@6jtOK6OeL zZ)&q^;+MaDTk81r_o5vIV+TySl(gW^t5G{F5er`#`pUm9fLC_sp>vGx{oU8b{}i+M z#W{PgUu)U3vaGn5eQu9Ct!{1l>Wg{jC&t$^RyY?9ZZ^ErqAvZKm7G4J*WY|_SS9#^dB}mR{ytYSBs7fE~*QjTaSLs z_uYPv+IIbsO|w@vi`H*j_vohiEA`Psw{5xqg@+= z^q%$s{di#Yu*Wan>A3U0Iya-zF1AR0YirKO$D40=Kb)G|=GNG^MrYi4`$-FK&nU$$JplwEDC3Cc3<&IDSS{cfRxSN#$(~<_bCoP@E zH>BGC2O3KTOAc_%H4Q)Ke=%x@w$aPwQ6*OrPqZ$2t8r6J^ngAKem*$no7F+X+;cRu z^4M24y)di&#)I1*?EJ?ae(@hSoEP)nD~fD-VEhBk3NQQ^+Vrc9&Z&kA85!<-#$D?- z=J8<%<0{IgwCtD86Uj(!P{3fn+X0$+LIH9Tg7+UG8?H%v$DrHFWHevnj<(CycSrUEBHe?$RG$nA+==?gN7J z4Y?cVud>H4`?JofPo8h6QUEeiICz}k2O2nfO2N2G;6VX2z_cp-CxP*Yi&^m4sskI| ziP}5C-|Z?*{sb@(ut1;%hLJrWga8Z-*#a!Up9F45Du5ms?uB~-NYE=HbjXSKm=m7? zdJ|LN10}E~j4hG_5~LuY!1BHA#kP6-f>#nYrl1L?BV#~;6Qp6S-ZB&b1=jARIY_=1 zX7(U?|5_NV=~rbs`Z%&Ib_$=6@@O6hbj|J-U#>NB@hq-Une` zT*hc029owkV7yx2o!%-zLn5vIS&etA1jegP0GRsMbTb5jN?QFW#a0N7-U$(Rt$HVf z9(I0=g?`E`Z&fple}jwKvy`}3GpI!EcK}VKBUm!G(quL%ppqwM-A`)*Us8fe92LCA z$fQ4bO~g!LbZZ*Co(C}L=|)}4|IFL-#7eVi0h}@)!n|B!+0%N?Q|C+Tlpfms$c{Pr zXJ5J#mDFz4l$M#1Pb3_FZ^_k5+s|7rC12mQ`uD~0*B9t+=gdt1>`uRjOI&v@pIw#_ zwr)bJo4MWRW_-K6b$R*csU6p(d^Tv~hFM?VwpY$Rck|5l3*53l;*Vb`8FPMtqvDLM zh2x^7*RABICe6%gaq?nm>xmbFDk`0=Uhe#B;f6i)OYG-%e zQv7H67Y9z}pBlGi=%FXB$#U6GfxS2&HaYo+jj4Y-R+s|NZ01D@hkJ| zw%mR1r`Ec#f%$9iuYYN~RZRc<_WPQYYO?FD-}YSe@+uy?U4CTW^7~UB+W4qDw5-R$ z`77H;_n(qd*WKiomOr#q{hI&K+|+s47T5d3-&g*M>cs|qS&>)qW$VU~;o}ppEGZj$ zsJQ5{*sw2qHGVt1)3fe(%eo$#et6)S%(5K^ryu(M2geoTjo{l4SH?|xsko}g1I?Z~ z@6ne=GYzAyWv1_Ta&kHYZx&GxIeFX?0*efZhOr#`(W ze6MA2`mvzy)=>VbwWAATDkrXeY|x45Et1_(o!3uh6!kl~;!xo4$P4;$!xn`-AGWS+Q)_!d>PA%#n&g zGZM?oBW7+W=yIZwz1Y0-n?aL$PDorGJhMmax;8Dcwk6N({K&cSl`374MvWjiQSgR} zL`qq_J6C(A+cWfC94^@IH<&=06n!6CsvCAZGMo;U*ai3_FoI5i6?YxF(a|7XN&3Eh zp243se5Bj$?5NkncXD*FbnI-EGtV8)gOn#K3!X7jt_y%~o4AY+T@2MrYQt zW`oWM_hy|LYKE!Mtqr$3T+SE=?81`_L26)4AURs0W;IY2I$#YVYXspJ%&37Ph$^@tjHH4-0TOso z)&Xj230ORKfRxSgoK6f}c5M_`;(*8)d>jxV;D**iYPx#pYoom&JyOxI^%&GRP|>wl zT2J$dVGucm(!w5Q*_hWxX<^KzvN^^Kav!kl+AFQd`;}JYX|)1-V>ZiLUK@?YmXo46 zh^quDxc15_3Vvl3MOs;tN7@{&vAxrJ!#*X8JKbTYyTH~)V^M5KHLO7Zi&>j$YNxiu z;){~n;@lz{6l%*GWu2&v)?z0}(IAG{i2y~{MrnEAFYI!Ia#2bRcn=iB!qrdtc2}m( zZW%LHmzAEWPj_4O_S9^>)8(+{q`I^9FjZ&CVF2u@uelwEz8YY1GicGoidq9OuNJLQ zRP3*pfbJLzZ0dK6(C8Ck63Et~qCuz?A9`Ab+cLAY5fQMFEhj5ms~xO?mjv{51w50Y zg+F>+-_6Fw1%JK-JaMA$n{Dew&U}Ep+M`oZV6}ru>_w{|FmMngHwidjFfgnMHn+tJ z?a)d=96wm1)Sfh!v=J7H9XWWcL8FgPx5AMWsZ^f0Cx8X8C1BCXMU529c@C6;; z!J@I>u)yT@jzWUXnr2O759gfJ||<#1O2wo9hXX085pvMW8S`nP>- z*>F4t58QaXD*$?KXc_dDg}rsyLzL(~S`UUY1lqz{UUerCcJDxce-(MmWlI z#yUmWKV;T_EGDvk{F2G{zPRv18)x9V5JI($od=jh!>xFFz@D}fg_gA(MjYm0qdr z6!$5XQ+%el?-LINJFy)~c6yrom$K8_ONyOnbp8qK6g{?-W~XHA$xayAJVSyfa-&YN zd-|zRb0vc*uKM^#*=?Cl1FQ?6-n14|W9`*q-^B43YB8w;`|f8uK^^}HwyU*RnqUD> zgXAih?3HhrPixL(N?$20rdaPY+;S}@?eL7DkDcBb|F2{x)}df0C0T##sTqz(6P!e8 zv4o7^w4sWfB(Z0J1F+M_B7GdCz7%JD>{Rd&wO7f*{{whMFrb`=R7;SBu-xhOIlNI= za7z+;!l7~Lc(REXp)a0O$Pt}$;lw)}6W0#6jYzjghJ|U(2A)aEbslHO3IpC&ZnbuT2968TIk3!GtSUnbHP3`DThm-J8eeL9Gjeukx5_y}7 zC+{6DPpwI|(Vn}^bag{Klrfgf94s)y?s(oFp~u!qB#rbyH63!0_oOs(C)I$B@YhUU z$a7(#6s4*ma8e^{&(;w}=-{gbdUyf-=SlzJz9T3Ccb=Q&riE8ng#Q!^aa@dvYyKXb(f$1Bp7uNJF0NaAf*NQTYOd&70KL6;9Dd zaSR->Ffqnt2#GVIVNir#kwD4;j(U39*t_JQ5>(k!!VHjKWSvjm;lZaa;M+uoxlkXf z@PW{i^B4zUA9%1VBiS!{QukV8m~*61!(dIgFX|31po9;?Wsz)f5(#$EgsWp_kVWz! z!==zGJWB>C9KwPa(ZmyJ#EK#T;#2TUyc-QZW0)BTl5cRTw>KzS?rS^LV0U{ZaBSiuvBsOP&#R2F`3N<8jsIRMbuqV*r&%(hPl_Cr2GF1zi z3;a0*!KljNbx>j4p%PDQ#h-;YusZ0DtX&j5s49y9ku)e(mW6V>Ad9l|f=Qjj>oQ60 zQg>bi?$T8d)=*t1f+0nq$NX)Zu%qBdfDa*^41k`L94Ga`+v69nRGsX+#G!Zw4 zDQHlIvR>9?Kt-14H9<#IGy%^3D%0^8s8ZzRl>G&Muo#+9q+{Sy2nw3fjEXEzXEY%O zQ_!mDhtYuNUKQy?Jfxza2`1IJ7JiujurR9f%&PtZk5sY& zJVH|~YYU8uI-Nmfhgim7#7R+}*T|`4oG}`d{eUqws^p$AnsKj_ygU=H96y#Z;pagV z>EK&w%JBnBhe2|Ad1e#3*fJVyRZ)%=9Q)wS8eg36fHmnNU zX+T&cs{=mlpprE}3`$!J_JG6Bbjb6v2E<4Tnh}ALoW?OKdEwyPrn0{vR_eUG0k$10 z%Hxey&_q00C9jtkRQQGCjjDNp;{`m|rpOD5uA=cO{or^+YQDT~mnA(DK1v6$G%0={Gxjux76uQOuKG9}ZY?kY@1N;9ID!%5jB!Rrj5Tt`m>7nS>tX`*6UTNj#_~dp e$V3HBK^%!}x5ecqU=p+w@S-L(G`4q~=6?X!=Fxco literal 0 HcmV?d00001 diff --git a/norp/doc/norpUserGuide.xml b/norp/doc/norpUserGuide.xml new file mode 100644 index 0000000..a79330a --- /dev/null +++ b/norp/doc/norpUserGuide.xml @@ -0,0 +1,567 @@ + + + +
+ + <inlinemediaobject> + <imageobject> + <imagedata align="center" fileref="resources/ProteanLogo.png" + scale="30"/> + </imageobject> + </inlinemediaobject> <emphasis>norp</emphasis> User's Guide + + norp User's Guide + + + The NRL Nack-Oriented Proxy (norp) project + includes software for an RFC 1928 SOCKS5-compatible proxy server daemon + that is able to use the RFC 5740 Nack-Oriented Reliable Multicast (NORM) + transport protocol for efficient and robust data transfer between + norp proxy instances. The norp + proxy automatically supports conventional SOCKS TCP proxy operation when + a remote norp peer is unavailable. This software + was developed by the Naval + Research Laboratory (NRL) PROTocol Engineering Advanced + Networking Research Group. + The NRL reference implementation of NORM used here is available from + http://www.nrl.navy.mil/itd/ncs/products/norm. + + + + + Overview + + The norp application .... + + + + Theory of Operation + + The norp program acts as a SOCKS proxy server. + It supports the SOCKS5 "CONNECT", "BIND" and "UDP-ASSOCIATE" proxy methods + for conventional SOCKS proxy operation. The current + norp implementation does not require (or support) any + client authentication. Future versions may provide authentication or other + access control mechanisms. The current norp + implementation only provides NORM transport for the SOCKS TCP "CONNECT" + requests. SOCKS "UDP-ASSOCIATE" over NORM will be supported in a future + version. + + Unlike a conventional SOCKS server, it is expected that the + norp daemon can be installed and run as a local + "loopback" server that is co-resident on the host running applications + that wish to take advantage of NORM transport benefits. The + norp daemon implements its own signaling protocol + that will automatically determine, upon TCP (or UDP) connection + establishment, if a remote destination is also similarly + "norp-enabled" and establish a NORM transport + connection as the proxy connection. Otherwise a "business-as-usual" TCP + (or UDP) connection is established on the application's behalf and thus + compatibility with "non-norp" hosts is also + supported. illustrates this high level + concept of operation. + +
+ NORP Concept of Operation + + + + + + +
+ + TBD - provide some more details on norp signaling for peer + detection and NORM session establishment + + Note that as an alternative to making proxied connections directly + to connection destination addresses as illustrated above, a remote + norp peer "correspondent" can be specified as part of + the forward command or, for SOCKS connections, with the + correspondent command (see command descriptions below). + Future versions of norp will include more + sophisticated "routing" options for different destinations and traffic + types. + + + SOCKS Loopback and Intermediate System + + As noted above the principal use case for + norp is to act as a local, "loopback" SOCKS server + that can be used in conjunction with a properly configured SOCKS client. + In this way, all of the configuration parameters are localized and + implicit and no precoordinated configuration with + norp peers (or non-norp hosts) + is required other than using a common UDP port number for NORP + signaling. + + However, there may be use cases where it may be desirable to + deploy norp on intermediate systems at the + connection originating site (or domain) and/or the destination site(s) + (or domain(s)). This is easily supported by the + norp design and future norp versions will provide + configuration options for this type of deployment. + + + + NORM Protocol Usage + + TBD - describe how the NORM streaming capability is used + in a flow-controlled, positively-acknowledged fashion to provide a + reliable TCP proxy function. Also describe the NORM congestion control + options here. + +
+ + + Usage + + Typically, norp can be run in its default + configuration with no command-line options required. However, a number of + options are available via the command-line. This is a summary of + norp usage: + + + norp [interface <ifaceName>][address <publicAddr>][sport <socksPort>][port <norpPort>] + [norm {on|off}][id <normId>][nport <normPort>][cce | ccl | rate <bits/sec>] + [limit <bits/sec>][persist <seconds>][correspondent <remoteNorpAddr>] + [forward <tcpPort>,<destAddr>/<destPort>[,<remoteNorpAddr>]][version][debug <level>] + [trace][dlog <debugLog>][lport <localNorpPort>][rport <remoteNorpPort>] + + The norp program command-line options + include ... + + + + <emphasis><emphasis>norp</emphasis></emphasis> Command-line + Options + + + + + + + + + + + interface + <interfaceName> + + The given <interfaceName> + specifies the name (or IP address) of the host network interface + norp uses as its "public" proxy address. + Currently a single interface may be designated for an instance of + norp. Future version of + norp may allow for multiple interfaces to be + designated depending upon the source and/or destination address of + SOCKS proxy connections. + + + + address <publicAddr> + + This is similar to the "interface" + command, but allows a specific address to be set. For example, + hosts with multiple addresses assigned may wish to use a specific + address for proxy functions. + + + + sport <socksPort> + + This command is used to specify the port number on which + the norp server listens for SOCKS client + connections. The default port is currently port number + 7000. + + + + port <norpPort> + + This command is used to specify the UDP port number used + for norp session setup signaling. The default + norp UDP signaling port is 7001. The + configured norp port number (and NORM port + number) MUST be unblocked by any network firewalls between + norp peers. The given port number is used by + norp to listen for remote connection request + and is used as the destination port to signal remote + norp peers. + + + + norm {on | off} + + By default, norp attempts to signal + the SOCKS connection endpoint to setup a NORM transport connection + to handle reliable data transfer for the TCP connection being + instantiated. This command with the "off" + argument will disable this function and norp + will act as a conventional SOCKS proxy server. + + + + id <normId> + + By default, norp will attempt to + self-configure a NORM protocol node identifier using the IP + address of the server host. This command allows a specific NORM + node identifier value to be set. It is generally not necessary to + explicitly set this value for norp unicast + proxy connections. + + + + nport <normPort> + + This command can be used to specify a UDP port number that + will be used for NORM protocol transport connections. The default + NORM port number used by norp is 7002. The + configured NORM port number (and norp UDP + signaling port number) MUST be unblocked by any + network firewalls between norp peers. + + + + cce + + This option enables NORM-CCE congestion control operation + that uses Explicit Congestion Notification (ECN) information for + NORM protocol end-to-end transmission rate adaption. This is an + alternative to the TCP-friendly congestion control mechanism used + for NORM by default. Routers in the path of the + norp peers using the NORM-CCE option MUST be + configured for ECN packet marking in response to + congestion. + + + + ccl + + This option enables experimental NORM-CCL ("Loss Tolerant") + congestion control operation that uses some simple heuristics to + try to differentiate packet loss due to congestion versus duo to + channel bit errors. This is another alternative to the + TCP-friendly congestion control mechanism used for NORM by + default. No special intermediate system configuration is required, + and while more loss tolerant than the default TCP-friendly + behavior, is not as effective as the NORM-CCE mode of + operation. + + + + rate <bits/sec> + + This option causes norp to use a + preset and fixed transmission rate for each proxied data flow + (e.g. TCP connection). This should only be used when the network + connectivity usage is carefully pre-planned and previsioned for + the expected (i.e. a priori known) flows. At + this time, one common transmission rate is used for all + flows. + + + + limit <bits/sec> + + This option sets a limit for the + cumulative transmit rate for + all flows that norp is + proxying. For automated congestion operation, this can also work + to "jump start" the usual "slow start" transport rate control by + setting the lower bound of rate adjustment based on the limit + <bits/sec> / <numFlows>. For + example, a single flow will immediately "jump" to close the full + limit rate, while the second of two flows would "jump" to half of + the "limit" rate. Also, by setting a limit based on a + priori connectivity information, this can avoid rate + adjustment "overshoot" and help congestion control operate more + effectively as compared to a "blind" situation. IMPORTANT: This + options should only be applied when the connectivity path is well + known and the impact of the lower bound enforcement here will not + adversely impact other network traffic flows. A future option may + be provided to further reduce or eliminate the lower bound + enforcement that would eliminate this concern in less controlled + network deployments. A limit value of "-1.0" (default) disables + the limit enforcement. + + + + correspondent + <remoteNorpAddr> + + This option causes norp to "route" + connections through a norp peer at the + specified <remoteNorpAddr>. This is + an alternative to the default behavior where + norp attempts to connect directly to the + connection destination addresses. + + + + persist <seconds> + + This option controls how persistently norp attempts to + deliver data to the remote endpoint when the remote endpoint fails + to acknowledge reception. A persist value of -1 makes norp + infinitely persistent and the corresponding norp session remains + in place until all data is delivered. If not, an orphaned session + will remain in place if the remote endpoint is permanently + disconnected. The default persist value is 120 seconds (2 + minutes). + + + + forward + <tcpPort>,<dstAddr>/<dstPort>[,<norpAddr>] + + This command sets up a "preset" TCP proxy (non-SOCKS) port + forwarding session by listening on the specified TCP + <tcpPort> for connections and then + connecting to the given remote + <dstAddr>/<dstPort>. + Optionally, a separate remote + <norpAddr> may be given. Otherwise, a + norp proxy connection is attempted to the + given <dstAddr> platform on the + norp <port> (or + <rport> if specified). Note that + multiple such "preset" proxy sessions may be specified on the + command-line and each "preset" proxy session can handle multiple + connections as needed. + + + + debug <debugLevel> + + + + + This command can be used to control the verbosity of + norp debug logging output. Generally, + the range of the value is 0-12. A higher value results in + more verbose, detailed debug output. + + + + + + + trace + + This command enables NORM send and receive packet trace + logging. + + + + dlog <fileName> + + + + + This command can be used to direct + norp debug logging output to a given + file. The default norp debug logging is + to STDERR. + + + + + + + lport <localNorpPort> + + This command can enable single host, loopback testing by a + having norp listen on a different port number + than which it uses as the destination port for remote + norp peer signaling. E.g., two + norp instances on a single machine can be set + up with unique <localNorpPort> values and + then use the "rport" command to specify each + other's destination norp port + numbers. + + + + rport <remoteNorpPort> + + This command is intended to be used in conjunction with the + "lport" command to allow separate specification + of the destination port number used for remote + norp peer signaling. + + + +
+ + + SOCKS Client Configuration + + TBD - provide overview and examples (for specific SOCKS + clients of note such as Dante, Proxifier, etc) of SOCKS client + configuration + + + + Usage Examples + + The SOCKS client(s) must be configured to use the + norp server unless a preset TCP port + forward is specified. For example the Dante proxy + distribution (available from http://www.inet.no/dante/) has a + socksify command that is installed and can be used + to launch existing network applications so their socket communications + are directed through the configured server. With Dante, a SOCKS + configuration file (typically /etc/socks.conf) or + the SOCKS5_SERVER environment variable can be used + to set the server address and port number. + + The norp "server" is a lightweight module and + can be installed on the same end systems requiring the performance + benefits of NORM transport. In this case the SOCKS client server + configuration is the loopback address and norp + SOCKS port number (i.e. 127.0.0.1:7000). The locally + installed norp SOCKS server will signal remote network destinations + (e.g., upon TCP connection initiation) to determine if the destination + is norp-capable. If possible, it will establish a + NORM-connection to the remote norp correspondent + that connects to the final destination. Otherwise a direct TCP + connection (or UDP relay) will be made to the remote destination. + +
+ + + The "NORP" UDP Signaling Message Format + + The norp proxy uses UDP signaling to confirm + presence of a remote norp peer and to set up (and + tear down) NORM transport protocol sessions to support the proxied TCP + (and eventually UDP) transport connections. The norp instance originating + a SOCKS session request is referred to here as the "originator" and the + remote norp peer to which the request is directed is referred to as the + "correspondent". The norp "originator" is the server + associated with the SOCKS client making a request while the + "correspondent" establishes connections with the remote SOCKS + destination. + + The following UDP payload format is used for NORP signaling: + + 0 1 2 3 +0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| msgType | reserved | sessionId | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| normNodeId | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| normSrcPort | normDstPort | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| timestamp_sec | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| timestamp_usec | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| content ... ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + + + The NORP message types include: + + + + SOCKS_REQ + + + The message content contains a SOCKS5 Request message from the + "originator" to the "correspondent" norp + server. + + + + + ACK_REQ + + + The message is used to acknowledge receipt of a SOCKS_REQ + message. There is no "content" + + + + + SOCKS_REP + + + The message content contains a SOCKS5 Reply message from the + "correspondent" norp server. + + + + + ACK_REP + + + The message is used to acknoledge receipt of a SOCKS_REP + message. There is no "content". + + + + + ORIG_END + + + This message indicates the "originator" + norp server is terminating the given session. + There is no "content". + + + + + CORR_END + + + This message indicates the "correspondent" + norp server is terminating the given session. + There is no "content". + + + + + ACK_END + + + This message is used to acknowledge receipt of either an + ORIG_END or CORR_END message. There is no "content". + + + + + TBD - describe NORP signaling and the message format given + here. + + + + Future Plans + + There are a number of additional features and refinements planned + for the norp implementation. Some of these + include: + + + + Source / destination configuration and "routing" options + + + + Data compression options + + + + Security features + + + +
diff --git a/norp/doc/resources/ProteanLogo.png b/norp/doc/resources/ProteanLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..4422ee1e294570ae5cbe927592972a2042ada217 GIT binary patch literal 28382 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 P3SLBABb{ejc9j1BA$@3- literal 0 HcmV?d00001 diff --git a/norp/include/norp.h b/norp/include/norp.h new file mode 100644 index 0000000..3872131 --- /dev/null +++ b/norp/include/norp.h @@ -0,0 +1,644 @@ +#ifndef _NORP +#define _NORP + +#include "protoSocket.h" +#include "protoTimer.h" +#include "protoTree.h" +#include "protoDispatcher.h" +#include "protoPktSOCKS.h" +#include "normApi.h" + +class Norp; + +// A "NorpSession" associates a SOCKS client session with either a remote +// NORP endpoint (if one was or can be established) or a "direct connect" +// (TCP, UDP) if the remote endpoint is not NORP-enabled + +// SOCKS requests/replies relayed via NORM command transmissions +// are prepended with this to uniquely identify the command +// (SOCKS request or reply and associate it with a NorpSession +// TBD - or should the endpoint socket binding addrs/ports be used instead?? + +// +// ProtoPktNORP is the packet format that NORP uses to relay SOCKS +// requests and replies via the NORM application-defined command +// signaling mechanism. The format of these commands is: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | msgType | reserved | sessionId | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | normNodeId | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | normSrcPort | normDstPort | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | timestamp_sec | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | timestamp_usec | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Content ... | +// + + +class PortPool +{ + public: + PortPool(UINT16 basePort); + + UINT16 GrabPort(); + void ReleasePort(UINT16 thePort); + + private: + UINT16 base_port; + bool port_array[50]; +}; // end class PortPool + +class NorpMsg : public ProtoPkt +{ + public: + NorpMsg(UINT32* bufferPtr = NULL, + unsigned int numBytes = 0, + bool initFromBuffer = true, + bool freeOnDestruct = false); + ~NorpMsg(); + + bool InitFromBuffer(unsigned int msgLength, + UINT32* bufferPtr = NULL, + unsigned int numBytes = 0, + bool freeOnDestruct = false) + {return ProtoPkt::InitFromBuffer(msgLength, bufferPtr, numBytes, freeOnDestruct);} + + enum Type + { + MSG_INVALID = 0, + SOCKS_REQ, // "content" is a SOCKS5 request + REQ_ACK, // request acknowledgment (no content) + SOCKS_REP, // "content" is a SOCKS5 request + REP_ACK, // reply acknowledgment (no content) + ORIG_END, // sent by originator to end session + CORR_END, // sent by correspondent to end session + ACK_END // end session acknowledgment (by either) + }; + + Type GetType() const + {return (Type)((UINT8*)buffer_ptr)[OFFSET_TYPE];} + + UINT16 GetSessionId() const + {return (Type)ntohs(((UINT16*)buffer_ptr)[OFFSET_SESSION_ID]);} + + // note "GetUINT32()" does ntohl() conversion + NormNodeId GetNodeId() const + {return (NormNodeId)GetUINT32(buffer_ptr + OFFSET_NODE_ID);} + + UINT16 GetSourcePort() const + {return (Type)ntohs(((UINT16*)buffer_ptr)[OFFSET_SRC_PORT]);} + + UINT16 GetDestinationPort() const + {return (Type)ntohs(((UINT16*)buffer_ptr)[OFFSET_DST_PORT]);} + + void GetTimestamp(struct timeval& timestamp) const + { + timestamp.tv_sec = GetUINT32(buffer_ptr + OFFSET_SEC); + timestamp.tv_usec = GetUINT32(buffer_ptr + OFFSET_USEC); + } + + void GetTimestamp(ProtoTime& theTime) const + { + struct timeval& timestamp = theTime.AccessTimeVal(); + timestamp.tv_sec = GetUINT32(buffer_ptr + OFFSET_SEC); + timestamp.tv_usec = GetUINT32(buffer_ptr + OFFSET_USEC); + } + + const UINT32* GetContentPtr() const + {return ((const UINT32*)AccessContentPtr());} + + UINT32* AccessContentPtr() const + {return (buffer_ptr + OFFSET_CONTENT);} + + unsigned int GetContentLength() const + {return (GetLength() - (OFFSET_CONTENT*4));} + + // Packet building methods + + void Init() + { + SetType(MSG_INVALID); + SetLength(0); + } + + void SetType(Type msgType) + { + ((UINT8*)buffer_ptr)[OFFSET_TYPE] = (UINT8)msgType; + pkt_length = (pkt_length < 1) ? 1 : pkt_length; + } + + void SetSessionId(UINT16 sessionId) + { + ((UINT8*)buffer_ptr)[OFFSET_RESERVED] = 0; + ((UINT16*)buffer_ptr)[OFFSET_SESSION_ID] = htons(sessionId); + pkt_length = (pkt_length < 4) ? 4 : pkt_length; + } + + void SetNodeId(NormNodeId nodeId) + { + SetUINT32(buffer_ptr + OFFSET_NODE_ID, nodeId); + pkt_length = (pkt_length<8) ? 8 : pkt_length; + } + + void SetSourcePort(UINT16 srcPort) + { + ((UINT16*)buffer_ptr)[OFFSET_SRC_PORT] = htons(srcPort); + pkt_length = (pkt_length < 10) ? 10 : pkt_length; + } + + void SetDestinationPort(UINT16 dstPort) + { + ((UINT16*)buffer_ptr)[OFFSET_DST_PORT] = htons(dstPort); + pkt_length = (pkt_length < 12) ? 12 : pkt_length; + } + + void SetTimestamp(struct timeval& timestamp) + { + SetUINT32(buffer_ptr+OFFSET_SEC, timestamp.tv_sec); + SetUINT32(buffer_ptr+OFFSET_USEC, timestamp.tv_usec); + pkt_length = (pkt_length < 20) ? 20 : pkt_length; + } + + bool SetContent(const char* data, unsigned int numBytes) + { + if ((numBytes + (4*OFFSET_CONTENT)) > GetBufferLength()) return false; + memcpy((char*)(buffer_ptr+OFFSET_CONTENT), data, numBytes); + SetContentLength(numBytes); + return true; + } + + void SetContentLength(unsigned int numBytes) + {pkt_length = (4*OFFSET_CONTENT) + numBytes;} + + private: + enum + { + OFFSET_TYPE = 0, // UINT8 offset + OFFSET_RESERVED = OFFSET_TYPE + 1, // UINT8 offset + OFFSET_SESSION_ID = (OFFSET_RESERVED+1)/2, // UINT16 offset + OFFSET_NODE_ID = (2*(OFFSET_SESSION_ID+1))/4, // UINT32 offset + OFFSET_SRC_PORT = (4*(OFFSET_NODE_ID+1))/2, // UINT16 offset + OFFSET_DST_PORT = OFFSET_SRC_PORT + 1, // UINT16 offset + OFFSET_SEC = (2*(OFFSET_DST_PORT+1))/4, // UINT32 offset + OFFSET_USEC = OFFSET_SEC + 1, // UINT32 offset + OFFSET_CONTENT = OFFSET_USEC + 1 // UINT32 offset + }; + +}; // end class NorpMsg + + + +// A NorpPreset is a non-SOCKS proxied session where a +// local TCP port is monitored and connections are proxied +// proxied to a preset remote destination +class NorpPreset : public ProtoTree::Item +{ + public: + NorpPreset(Norp& norpController, UINT16 tcpPort, const ProtoAddress& dstAddr, const ProtoAddress& norpAddr); + ~NorpPreset(); + + const ProtoAddress& GetDstAddr() const + {return dst_addr;} + const ProtoAddress& GetNorpAddr() const + {return (norp_addr.IsValid() ? norp_addr : dst_addr);} + + bool Listen(); + + ProtoSocket& AccessServerSocket() + {return server_socket;} + + private: + const char* GetKey() const + {return ((const char*)&tcp_port);} + unsigned int GetKeysize() const + {return (sizeof(UINT16) << 3);} + + + void OnSocketEvent(ProtoSocket& theSocket, ProtoSocket::Event theEvent); + + Norp& controller; + UINT16 tcp_port; + ProtoAddress dst_addr; + ProtoAddress norp_addr; + ProtoSocket server_socket; + +}; // end class NorpPreset + +class NorpPresetList : public ProtoTreeTemplate +{ + public: + NorpPreset* FindPreset(UINT16 tcpPort) const + {return Find((char*)&tcpPort, sizeof(UINT16) << 3);} +}; // end class NorpPresetList + +class NorpSession : public ProtoTree::Item +{ + public: + NorpSession(Norp& norpController, UINT16 sessionId, NormNodeId originatorId = NORM_NODE_NONE); + ~NorpSession(); + + enum SocksState + { + SOCKS_VOID, // when session gives self back to "controller" at Close + SOCKS_IDLE, // initial, inactive state (socks_client_socket is closed) + SOCKS_GET_AUTH_REQ, // first active state, accumulating auth request bytes from client + SOCKS_PUT_AUTH_REP, // next, writing auth reply bytes to client + SOCKS_GET_REQUEST, // next, accumulate SOCKS request bytes from client + SOCKS_PUT_REQUEST, // relaying request to remote NORP peer + SOCKS_CONNECTING, // attempt to satisfy request via remote connect, etc (this may need to be refined) + SOCKS_GET_REPLY, // awaiting reply from remote NORP peer + SOCKS_PUT_REPLY, // next, writing SOCK reply bytes to client + SOCKS_CONNECTED, // finally, reading/writing data from/to client + SOCKS_SHUTDOWN // session ended, in process of shutting down + }; + + void SetProxyAddress(const ProtoAddress& proxyAddr) + {proxy_addr = proxyAddr;} + + void SetNormRateBounds(double rateMin, double rateMax); + + NormNodeId GetNodeId() const; + + UINT16 GetSessionId() const + {return session_id.identifier;} + + NormNodeId GetOriginatorId() const + {return session_id.originator;} + + bool IsRemoteSession() const + {return (NORM_SESSION_INVALID != norm_session);} + + bool IsRemoteOriginator() const // assumes "IsRemoteSession()" + {return (NORM_NODE_NONE == GetOriginatorId());} + + bool AcceptClientConnection(ProtoSocket& serverSocket, bool normEnable = true); + + bool AcceptPresetClientConnection(NorpPreset& preset, bool normEnable); + + SocksState GetSocksState() const + {return socks_state;} + + const ProtoAddress& GetClientAddress() const + {return socks_client_socket.GetDestination();} + + void Shutdown(); // hard shutdown, including signaling remote via NORP messaging + + bool ShutdownComplete() const; // Tests for inband shutdown completion + + void Close(); // hard local shutdown and cleanup including session deletion by controller + + void OnNormEvent(NormEvent& theEvent); + + bool OnClientRequest(const ProtoPktSOCKS::Request request, const ProtoAddress& srcAddr); + + bool MakeDirectConnect(); // convert remote session to direct connect + + // handles request relayed from remote NORP server + bool OnRemoteRequest(const NorpMsg& theMsg, const ProtoAddress& senderAddr); + + bool OnRemoteRequestAcknowledgment(const NorpMsg& theMsg, const ProtoAddress& senderAddr); + + bool OnRemoteReply(const NorpMsg& theMsg, const ProtoAddress& senderAddr); + + bool OnRemoteReplyAcknowledgment(const NorpMsg& theMsg); + + typedef struct Moniker + { + NormNodeId originator; + UINT16 identifier; // TBD - make this UINT32? + } Moniker; + + static bool GetNormNodeAddress(NormNodeHandle nodeHandle, ProtoAddress& theAddr); + + static const double NORP_RTT_MIN; + static const double NORP_RTT_MAX; + static const double NORP_RTT_DEFAULT; + + private: + void ActivateTimer(ProtoTimer& theTimer); + + const char* GetKey() const + {return ((const char*)&session_id);} + unsigned int GetKeysize() const + {return (sizeof(Moniker) << 3);} + + void OnSocksClientEvent(ProtoSocket& theSocket, + ProtoSocket::Event theEvent); + + // Note our "norp_tx_socket" is "connected" so we get error message when "port unreachable" + bool SendMessage(const NorpMsg& msg) + { + unsigned int numBytes = msg.GetLength(); + return norp_tx_socket.Send((const char*)msg.GetBuffer(), numBytes); + } + + bool OnNorpMsgTimeout(ProtoTimer& theTimer); + + bool OnCloseTimeout(ProtoTimer& theTimer); + + void OnUdpRelayEvent(ProtoSocket& theSocket, + ProtoSocket::Event theEvent); + + // These methods are invoked as needed upon different events and SOCKS state transitions + bool GetClientAuthRequest(); + bool PutClientAuthReply(); + bool GetClientRequest(); + bool ConnectToRemote(const ProtoAddress& srcAddr); + bool BindRemote(UINT16 bindPort); + bool OpenUdpRelay(); + bool PutClientReply(); + bool GetClientData(); + bool PutClientData(); + + bool PutRemoteRequest(const ProtoPktSOCKS::Request& request, const ProtoAddress& remoteAddr); + bool OriginatorStartNorm(const ProtoAddress& senderAddr, + NormNodeId corrNormId, + UINT16 normSrcPort, + UINT16 normDstPort); + + bool PutRemoteReply(); + + void OnSocksRemoteEvent(ProtoSocket& theSocket, + ProtoSocket::Event theEvent); + // These methods are invoked as needed upon different events and SOCKS state transitions + bool GetRemoteData(); + bool PutRemoteData(); + + enum {SOCKS_BUFFER_SIZE = 16384}; + enum {NORP_BUFFER_SIZE = 512}; + enum {NORM_BUFFER_SIZE = (8192*1024)}; + + unsigned int WriteToNormStream(const char* buffer, unsigned int numBytes); + void FlushNormStream(bool eom, NormFlushMode flushMode); + unsigned int ComputeNormStreamBufferSegmentCount(unsigned int bufferBytes, UINT16 segmentSize, UINT16 blockSize); + + + // Member variables + Norp& controller; + bool is_preset; // special "preset" (non-SOCKS) proxy session + ProtoTimer close_timer; + Moniker session_id; + ProtoAddress proxy_addr; // address to advertise for remote connections + SocksState socks_state; + ProtoSocket socks_client_socket; // TCP connection from this server to SOCKS client + ProtoSocket socks_remote_socket; // TCP connection to remote when "direct connect" is used + ProtoSocket udp_relay_socket; // UDP relay socket (used for UDP_ASSOC requests) + ProtoAddress udp_client_addr; // address of local UDP_ASSOC client + + UINT32 client_buffer[SOCKS_BUFFER_SIZE/sizeof(UINT32)]; // usually data received from client + unsigned int client_pending; // total bytes pending reception or transmission + unsigned int client_index; // current index + UINT32 remote_buffer[SOCKS_BUFFER_SIZE/sizeof(UINT32)]; // usually for data received from remote + unsigned int remote_pending; // total bytes pending reception or transmission + unsigned int remote_index; // current index + + // NORM-related state + bool norm_enable; + ProtoSocket norp_tx_socket; // used to send NORP signaling messages + ProtoAddress norp_remote_addr; + ProtoTimer norp_msg_timer; + NorpMsg norp_msg; // current message being transmitted + UINT32 norp_msg_buffer[NORP_BUFFER_SIZE/sizeof(UINT32)]; + double norp_rtt_estimate; // from REQUEST<->ACK or REPLY<->ACK exchange + NormSessionHandle norm_session; + NormObjectHandle norm_tx_stream; + NormObjectHandle norm_rx_stream; + bool norm_rx_pending; + double persist_interval; // in seconds, to timeout NORM delivery persistence after local TCP disconnect + ProtoTime persist_start_time; // marks start of persistence timeout + double norm_rate_min; + double norm_rate_max; + + UINT16 norm_segment_size; + unsigned int norm_stream_buffer_max; + unsigned int norm_stream_buffer_count; + unsigned int norm_stream_bytes_remain; + bool norm_watermark_pending; + +}; // end class NorpSession + +class NorpSessionList : public ProtoTreeTemplate +{ + public: + NorpSession* FindSession(UINT16 sessionId, NormNodeId originatorId = NORM_NODE_NONE) const + { + NorpSession::Moniker id; + memset(&id, 0, sizeof(NorpSession::Moniker)); + id.originator = originatorId; + id.identifier = sessionId; + return Find((char*)&id, sizeof(NorpSession::Moniker) << 3); + } // end NorpSessionList::FindSession() +}; // end class NorpSessionList + + + +// The "Norp" class is an instance of a SOCKS server that manages +// SOCKS/NORM (or direct-connect) sessions +class Norp +{ + public: + Norp(ProtoDispatcher& theDispatcher); + ~Norp(); + + enum + { + DEFAULT_SOCKS_PORT = 7000, // SOCKS server TCP listen port used for NORP + DEFAULT_NORP_PORT = 7001, // Where NORP listens for UDP signaling, relayed commands, etc + DEFAULT_NORM_PORT = 7002 // Port used for NORM data transfer + }; + + static const double DEFAULT_TX_RATE; + static const double DEFAULT_PERSIST_INTERVAL; + + void SetSocksPort(UINT16 thePort) + {socks_port = thePort;} + + void SetProxyAddress(const ProtoAddress& proxyAddr) + {proxy_addr = proxyAddr;} + + const ProtoAddress& GetProxyAddress() const + {return proxy_addr;} + + bool StartServer(bool normEnable); + + void StopServer(); + + void SetLocalNorpPort(UINT16 thePort) + {norp_local_port = thePort;} + + void SetRemoteNorpPort(UINT16 thePort) + {norp_remote_port = thePort;} + + UINT16 GetNorpPort() const + {return norp_remote_port;} + + // In future, this will reference a "routing" + // table using the "destAddr" + bool GetRemoteNorpAddress(const ProtoAddress& destAddr, + ProtoAddress& norpAddr) const + { + norpAddr = norp_remote_addr.IsValid() ? norp_remote_addr : destAddr; + return true; + } + // This sets a remote "norp" peer address that is used instead of + // connecting directly to destination endpoints + // TBD - add more routing options than this + void SetRemoteNorpAddress(const ProtoAddress& remoteAddr) + {norp_remote_addr = remoteAddr;} + + void SetInitialRtt(double seconds) + {norp_rtt_init = seconds;} + + double GetInitialRtt() const + {return norp_rtt_init;} + + // NORM protocol parameters + void SetNormNodeId(NormNodeId nodeId) + {norm_node_id = nodeId;} + NormNodeId GetNormNodeId() const + {return norm_node_id;} + + NormInstanceHandle GetNormInstance() const + {return norm_instance;} + + void SetNormPort(UINT16 thePort) + {norm_port = thePort;} + UINT16 GetNormPort() const + {return norm_port;} + + /* + UINT16 GrabPort() + {return port_pool.GrabPort();} + void ReleasePort(UINT16 thePort) + {port_pool.ReleasePort(thePort);} + */ + + enum NormCC + { + NORM_CC, + NORM_CCE, + NORM_CCL, + NORM_FIXED + }; + + void SetNormCC(NormCC ccMode) + {norm_cc_mode = ccMode;} + NormCC GetNormCC() const + {return norm_cc_mode;} + + // For fixed-rate operation + void SetNormTxRate(double txRate) + {norm_tx_rate = txRate;} + double GetNormTxRate() const + {return norm_tx_rate;} + + // Sets upper bound on _cumulative_ NormSession + // transmit rates + void SetNormTxLimit(double txLimit) + {norm_tx_limit = txLimit;} + + double GetNormTxLimit() const + {return norm_tx_limit;} + + void SetNormTrace(bool enable) + {norm_trace = enable;} + bool GetNormTrace() const + {return norm_trace;} + + void SetNormSegmentSize(UINT16 numBytes) + {norm_segment_size = numBytes;} + UINT16 GetNormSegmentSize() const + {return norm_segment_size;} + + void SetNormBlockSize(UINT32 numSegments) + {norm_block_size = numSegments;} + UINT16 GetNormBlockSize() const + {return norm_block_size;} + + void SetNormParityCount(UINT16 numSegments) + {norm_parity_count = numSegments;} + UINT16 GetNormParityCount() const + {return norm_parity_count;} + + void SetNormParityAuto(UINT16 numSegments) + {norm_parity_auto = numSegments;} + UINT16 GetNormParityAuto() const + {return norm_parity_auto;} + + ProtoSocket::Notifier& GetSocketNotifier() const + {return static_cast(dispatcher);} + + void ActivateTimer(ProtoTimer& theTimer) + {dispatcher.ActivateTimer(theTimer);} + + bool SendMessage(const NorpMsg& msg, const ProtoAddress& dstAddr) + {return norp_rx_socket.SendTo((const char*)msg.GetBuffer(), msg.GetLength(), dstAddr);} + + void OnNorpSocketEvent(ProtoSocket& theSocket, + ProtoSocket::Event theEvent); + + void OnSessionClose(NorpSession& theSession); + + bool AddPreset(UINT16 tcpPort, const ProtoAddress& dstAddr, const ProtoAddress& norpAddr); + bool AcceptPresetClientConnection(NorpPreset& preset); + + private: + void AddSession(NorpSession& session); + void RemoveSession(NorpSession& session); + + void OnSocksServerEvent(ProtoSocket& theSocket, + ProtoSocket::Event theEvent); + + static void NormEventCallback(ProtoDispatcher::Descriptor descriptor, + ProtoDispatcher::Event theEvent, + const void* userData); + + void OnNormEvent(); + void OnRemoteRequest(const NorpMsg& theMsg, const ProtoAddress& senderAddr); + void OnRemoteReply(const NorpMsg& theMsg, const ProtoAddress& senderAddr); + + // Member variables + ProtoDispatcher& dispatcher; // TBD - make Norp multi-threaded w/ a pool of dispatchers + + ProtoSocket socks_server_socket; // (SOCKS clients connect to this) + UINT16 socks_port; + ProtoAddress proxy_addr; + UINT16 next_session_id; // used to dole out unique session ids + NorpSessionList session_list; // list of client and remote sessions + unsigned int session_count; + NorpPresetList preset_list; // list of preset proxies w/ listening TCP socket + + ProtoSocket norp_rx_socket; // UDP socket for receiving NORP signaling + UINT16 norp_local_port; // For loopback debugging, we allow "local" server port to be different than "remote" + UINT16 norp_remote_port; + ProtoAddress norp_remote_addr; // if unspecified, we proxy directly to connection destination address + double norp_rtt_init; // used as initial roundtrip estimate for NORP signaling + + bool norm_enable; + NormInstanceHandle norm_instance; + NormNodeId norm_node_id; + + // NORM parameters (including some for debugging + UINT16 norm_port; + NormCC norm_cc_mode; + double norm_tx_rate; // in bits/sec, only applicable for cc_mode == NORM_FIXED + double norm_tx_limit; // in bits/sec (_cumulative_ rate limit option) + char iface_name[64]; // Network interface + UINT16 norm_segment_size; // payload bytes per NORM_DATA message + UINT16 norm_block_size; // number of user data segments per FEC coding block + UINT16 norm_parity_count; // number of _computed_ parity segments per FEC coding block + UINT16 norm_parity_auto; // number of proactive (automatically sent) parity segments per block + + bool norm_trace; + + //PortPool port_pool; + +}; // end class Norp + +#endif // _NORP diff --git a/norp/include/norpVersion.h b/norp/include/norpVersion.h new file mode 100644 index 0000000..c3ad7d0 --- /dev/null +++ b/norp/include/norpVersion.h @@ -0,0 +1,3 @@ +#ifndef _NORP_VERSION +#define NORP_VERSION "0.1b5" +#endif // NORP_VERSION diff --git a/norp/include/protoPktSOCKS.h b/norp/include/protoPktSOCKS.h new file mode 100644 index 0000000..9624525 --- /dev/null +++ b/norp/include/protoPktSOCKS.h @@ -0,0 +1,379 @@ + +#ifndef _PROTO_SOCKS +#define _PROTO_SOCKS + +#include "protoPkt.h" +#include "protoAddress.h" + +// These classes provide message parsing and building for +// the RFC 1928 SOCKS5 protocol specification. +// NOTES: +// 1) Only the methods needed for SOCKS server operation are defined here so far +// 2) Additional external helper logic is needed to incrementally read data from a +// SOCKS TCP client-server socket for a full SOCKS message. The message parsing +// here assumes the message has been read up to the point of whatever field +// is being requested. + +namespace ProtoPktSOCKS +{ + // SOCKS5 message definitions per RFC1928 + + // SOCKS5 auth request: VERSION + NMETHODS + METHOD_LIST ... + // 1 byte + 1 byte + N bytes + + // SOCKS5 authentication methods + enum AuthType + { + AUTH_NONE = 0x00, + AUTH_GSSAPI = 0x01, + AUTH_USER = 0x02, // username / password + AUTH_INVALID = 0xff // no acceptable method + }; + + class AuthRequest : public ProtoPkt + { + public: + AuthRequest(UINT32* bufferPtr = NULL, + unsigned int numBytes = 0, + bool initFromBuffer = true, + bool freeOnDestruct = false); + ~AuthRequest(); + + bool InitFromBuffer(unsigned int reqLength, + UINT32* bufferPtr = NULL, + unsigned int numBytes = 0, + bool freeOnDestruct = false) + {return ProtoPkt::InitFromBuffer(reqLength, bufferPtr, numBytes, freeOnDestruct);} + + UINT8 GetVersion() const + {return ((pkt_length > OFFSET_VERSION) ? + (((UINT8*)buffer_ptr)[OFFSET_VERSION]) : 0);} + + UINT8 GetMethodCount() const + {return ((pkt_length > OFFSET_NMETHOD) ? + (((UINT8*)buffer_ptr)[OFFSET_NMETHOD]) : 0);} + + AuthType GetMethod(UINT8 index) const + {return ((pkt_length > (unsigned int)(OFFSET_METHODS + index)) ? + (AuthType)(((UINT8*)buffer_ptr)[OFFSET_METHODS + index]) : AUTH_INVALID);} + + // TBD - implement auth request building (don't need this for a server) + + private: + enum + { + OFFSET_VERSION = 0, // 1 byte + OFFSET_NMETHOD = OFFSET_VERSION + 1, // 1 byte + OFFSET_METHODS = OFFSET_NMETHOD + 1 // N bytes + }; + + }; // end class ProtoPktSOCKS::AuthRequest + + // SOCKS5 auth reply: VERSION + METHOD_SELECTED + // bytes: 1 + 1 + class AuthReply : public ProtoPkt + { + public: + AuthReply(UINT32* bufferPtr = NULL, + unsigned int numBytes = 0, + bool initFromBuffer = true, + bool freeOnDestruct = false); + ~AuthReply(); + + bool InitFromBuffer(unsigned int replyLength, + UINT32* bufferPtr = NULL, + unsigned int numBytes = 0, + bool freeOnDestruct = false) + {return ProtoPkt::InitFromBuffer(replyLength, bufferPtr, numBytes, freeOnDestruct);} + + UINT8 GetVersion() const + {return ((pkt_length > OFFSET_VERSION) ? + (((UINT8*)buffer_ptr)[OFFSET_VERSION]) : 0);} + + UINT8 GetMethod() const + {return ((pkt_length > OFFSET_METHOD) ? + (((UINT8*)buffer_ptr)[OFFSET_METHOD]) : AUTH_INVALID);} + + void SetVersion(UINT8 version) + { + ((UINT8*)buffer_ptr)[OFFSET_VERSION] = version; + pkt_length = 1; + } + + void SetMethod(AuthType method) + { + ((UINT8*)buffer_ptr)[OFFSET_METHOD] = method; + pkt_length = 2; + } + + private: + enum + { + OFFSET_VERSION = 0, // 1 byte + OFFSET_METHOD = OFFSET_VERSION + 1 // 1 byte + }; + + }; // end class ProtoPktSOCKS::AuthReply + + // SOCKS5 request: VERSION + REQ + RESV + ADDR_TYP + DST.ADDR + DST.PORT + // bytes: 1 + 1 + 1 + 1 + N + 2 + + // SOCKS5 address types (1 byte) + enum AddrType + { + ADDR_INVALID = 0x00, // invalid addr type (for implementation) + IPv4 = 0x01, // 4-bytes, network byte order + NAME = 0x03, // 1-byte of "name" length, followed by "name" + IPv6 = 0x04 // 16-bytes, network byte order + }; // end ProtoPktSOCKS::AddrType + + class Request : public ProtoPkt + { + public: + Request(UINT32* bufferPtr = NULL, + unsigned int numBytes = 0, + bool initFromBuffer = true, + bool freeOnDestruct = false); + ~Request(); + + enum Command + { + CMD_INVALID = 0x00, + CONNECT = 0x01, + BIND = 0x02, + UDP_ASSOC = 0x03 // Setup UDP association + }; // end ProtoPktSOCKS::Request::Command + + bool InitFromBuffer(unsigned int replyLength, + UINT32* bufferPtr = NULL, + unsigned int numBytes = 0, + bool freeOnDestruct = false) + {return ProtoPkt::InitFromBuffer(replyLength, bufferPtr, numBytes, freeOnDestruct);} + + UINT8 GetVersion() const + {return ((pkt_length > OFFSET_VERSION) ? + (((UINT8*)buffer_ptr)[OFFSET_VERSION]) : 0);} + + Command GetCommand() const + {return ((pkt_length > OFFSET_COMMAND) ? + (Command)(((UINT8*)buffer_ptr)[OFFSET_COMMAND]) : CMD_INVALID);} + + AddrType GetAddressType() const + {return ((pkt_length > OFFSET_ATYPE) ? + (AddrType)(((UINT8*)buffer_ptr)[OFFSET_ATYPE]) : ADDR_INVALID);} + + UINT8 GetAddressLength() const; + + const char* GetAddressPtr() const + {return (const char*)AccessAddressPtr();} + + UINT16 GetPort() const; + + bool GetAddress(ProtoAddress& theAddr) const; + + // TBD - implement request building (don't need this for a server) + + void SetVersion(UINT8 version) + { + ((UINT8*)buffer_ptr)[OFFSET_VERSION] = version; + pkt_length = 1; + } + void SetCommand(Command cmd) + { + ((UINT8*)buffer_ptr)[OFFSET_COMMAND] = (UINT8)cmd; + pkt_length = 2; + } + void SetAddressType(AddrType addrType) + { + ((UINT8*)buffer_ptr)[OFFSET_RESERVED] = 0; + ((UINT8*)buffer_ptr)[OFFSET_ATYPE] = (UINT8)addrType; + pkt_length = 4; + } + bool SetAddress(const ProtoAddress& theAddr); // note this sets port, too + bool SetName(const char* theName); + bool SetPort(UINT16 thePort); + + private: + enum + { + OFFSET_VERSION = 0, // 1 byte + OFFSET_COMMAND = OFFSET_VERSION + 1, // 1 byte + OFFSET_RESERVED = OFFSET_COMMAND + 1, // 1 byte + OFFSET_ATYPE = OFFSET_RESERVED + 1, // 1 byte + OFFSET_ADDR = OFFSET_ATYPE + 1 // N bytes + }; + + char* AccessAddressPtr() const + { + char* addrPtr = ((char*)buffer_ptr) + OFFSET_ADDR; + return (NAME == GetAddressType()) ? (addrPtr + 1) : addrPtr; + } + + }; // end class ProtoPktSOCKS::Request + + // SOCKS5 REPLY: VERSION + REP + RESV + ATYP + BIND.ADDR + BIND.PORT + // bytes: 1 + 1 + 1 + 1 + N + 2 + class Reply : public ProtoPkt + { + public: + Reply(UINT32* bufferPtr = NULL, + unsigned int numBytes = 0, + bool initFromBuffer = true, + bool freeOnDestruct = false); + ~Reply(); + + enum Type + { + SUCCESS = 0x00, // succeeded + FAILURE = 0x01, // general failure + DENIED = 0x02, // connection not allowed + NET_X = 0x03, // network unreachable + HOST_X = 0x04, // host unreachable + REFUSED = 0x05, // connection refused + TTL_X = 0x06, // TTL expired + CMD_X = 0x07, // command not supported + ADDR_X = 0x08, // address type not supported + TYPE_INVALID = 0xff // invalid reply type + }; // end ProtoPktSOCKS::Reply::Type + + bool InitFromBuffer(unsigned int replyLength, + UINT32* bufferPtr = NULL, + unsigned int numBytes = 0, + bool freeOnDestruct = false) + {return ProtoPkt::InitFromBuffer(replyLength, bufferPtr, numBytes, freeOnDestruct);} + + UINT8 GetVersion() const + {return ((pkt_length > OFFSET_VERSION) ? + (((UINT8*)buffer_ptr)[OFFSET_VERSION]) : 0);} + + Type GetType() const + {return ((pkt_length > OFFSET_TYPE) ? + (Type)(((UINT8*)buffer_ptr)[OFFSET_TYPE]) : TYPE_INVALID);} + + AddrType GetAddressType() const + {return ((pkt_length > OFFSET_ATYPE) ? + (AddrType)(((UINT8*)buffer_ptr)[OFFSET_ATYPE]) : ADDR_INVALID);} + + UINT8 GetAddressLength() const; + + const char* GetAddressPtr() const + {return (const char*)AccessAddressPtr();} + + UINT16 GetPort() const; + + bool GetAddress(ProtoAddress& theAddr) const; + + // Reply building (call these in order + void SetVersion(UINT8 version) + { + ((UINT8*)buffer_ptr)[OFFSET_VERSION] = version; + pkt_length = 1; + } + void SetType(Type replyType) + { + ((UINT8*)buffer_ptr)[OFFSET_TYPE] = (UINT8)replyType; + pkt_length = 2; + } + void SetAddress(AddrType addrType, const char* addrPtr, UINT8 addrLen); + void SetPort(UINT16 thePort); + void SetAddress(const ProtoAddress& theAddr); // sets address and port from ProtoAddress + + private: + enum + { + OFFSET_VERSION = 0, // 1 byte + OFFSET_TYPE = OFFSET_VERSION + 1, // 1 byte + OFFSET_RESERVED = OFFSET_TYPE + 1, // 1 byte + OFFSET_ATYPE = OFFSET_RESERVED + 1, // 1 byte + OFFSET_ADDR = OFFSET_ATYPE + 1 // N bytes + }; + + char* AccessAddressPtr() const + { + char* addrPtr = ((char*)buffer_ptr) + OFFSET_ADDR; + return (NAME == GetAddressType()) ? (addrPtr + 1) : addrPtr; + } + + }; // end class ProtoPktSOCKS::Reply + + // UDP relay header: RESV + FRAG + ATYP + DST.ADDR + DST.PORT + DATA + // bytes: 2 + 1 1 + N + 2 + K + + class UdpRequest : public ProtoPkt + { + public: + UdpRequest(UINT32* bufferPtr = NULL, + unsigned int numBytes = 0, + bool initFromBuffer = true, + bool freeOnDestruct = false); + ~UdpRequest(); + + bool InitFromBuffer(unsigned int requestLength, + UINT32* bufferPtr = NULL, + unsigned int numBytes = 0, + bool freeOnDestruct = false) + {return ProtoPkt::InitFromBuffer(requestLength, bufferPtr, numBytes, freeOnDestruct);} + + UINT8 GetFrag() const + {return ((pkt_length > OFFSET_FRAG) ? + (((UINT8*)buffer_ptr)[OFFSET_FRAG]) : 0);} + + AddrType GetAddressType() const + {return ((pkt_length > OFFSET_ATYPE) ? + (AddrType)(((UINT8*)buffer_ptr)[OFFSET_ATYPE]) : ADDR_INVALID);} + + UINT8 GetAddressLength() const; + + const char* GetAddressPtr() const + {return (const char*)AccessAddressPtr();} + + UINT16 GetPort() const; + + bool GetAddress(ProtoAddress& theAddr) const; + + const char* GetDataPtr() const + {return (const char*)AccessDataPtr();} + + unsigned int GetDataLength() const + {return (GetLength() - (GetDataPtr() - ((const char*)buffer_ptr)));} + + + // Packet building (call these in order) + void SetFragment(UINT8 fragId) + { + ((UINT8*)buffer_ptr)[OFFSET_RESERVED] = 0; + ((UINT8*)buffer_ptr)[OFFSET_FRAG] = fragId; + pkt_length = OFFSET_FRAG + 1; + } + void SetAddress(AddrType addrType, const char* addrPtr, UINT8 addrLen); + void SetPort(UINT16 thePort); + void SetAddress(const ProtoAddress& theAddr); // sets address and port from ProtoAddress + void SetData(const char* data, unsigned int numBytes) + { + memcpy(AccessDataPtr(), data, numBytes); + SetDataLength(numBytes); + } + void SetDataLength(unsigned int numBytes) + {pkt_length = (GetDataPtr() - ((char*)buffer_ptr)) + numBytes;} + + private: + enum + { + OFFSET_RESERVED = 0, // 2 bytes + OFFSET_FRAG = OFFSET_RESERVED + 2, // 1 byte + OFFSET_ATYPE = OFFSET_FRAG + 1, // 1 byte + OFFSET_ADDR = OFFSET_ATYPE + 1 // N bytes + }; + char* AccessAddressPtr() const + { + char* addrPtr = ((char*)buffer_ptr) + OFFSET_ADDR; + return (NAME == GetAddressType()) ? (addrPtr + 1) : addrPtr; + } + char* AccessDataPtr() const + {return (AccessAddressPtr() + GetAddressLength() + 2);} + + }; // end class ProtoPktSOCKS::UdpRequest + +}; // end namespace ProtoPktSOCKS + +#endif // _PROTO_SOCKS diff --git a/norp/makefiles/Makefile.common b/norp/makefiles/Makefile.common new file mode 100644 index 0000000..b8112e0 --- /dev/null +++ b/norp/makefiles/Makefile.common @@ -0,0 +1,54 @@ +######################################################################### +# "norp" common Makefile portion +# + +SHELL=/bin/sh + +.SUFFIXES: .cpp $(.SUFFIXES) + +# "chant" depends upon the NRL "norm" and "protolib" libraries +NORM = ../norm +PROTOLIB = $(NORM)/protolib +LIBPROTO = $(PROTOLIB)/lib/libprotokit.a +LIBNORM = $(NORM)/lib/libnorm.a + +COMMON = ../src/common + +NORP_SRC = $(COMMON)/norp.cpp $(COMMON)/norpApp.cpp \ + $(COMMON)/protoPktSOCKS.cpp $(SYSTEM_SRC) + +NORP_OBJ = $(NORP_SRC:.cpp=.o) + +INCLUDES = -I../include -I$(NORM)/include -I$(PROTOLIB)/include + +CFLAGS = -g -DPROTO_DEBUG -Wall -O -fPIC $(SYSTEM_CFLAGS) $(PROTO_CFLAGS) $(INCLUDES) + +LIBS = $(SYSTEM_LIBS) + +TARGETS = norp + +# Rule for C++ .cpp extension +.cpp.o: + $(CC) -c $(CFLAGS) -o $*.o $*.cpp + + +all: norp + +$(PROTOLIB)/lib/libprotokit.a: + $(MAKE) -C $(PROTOLIB)/makefiles -f Makefile.$(SYSTEM) libprotokit.a + +$(NORM)/lib/libnorm.a: + $(MAKE) -C $(NORM)/makefiles -f Makefile.$(SYSTEM) libnorm.a + +norp: $(LIBPROTO) $(LIBNORM) $(NORP_OBJ) + $(CC) $(LDFLAGS) -o $@ $(NORP_OBJ) $(LIBS) $(LIBNORM) $(LIBPROTO) + +clean: + rm -f norp $(COMMON)/*.o; + $(MAKE) -C $(NORM)/makefiles -f Makefile.$(SYSTEM) clean; + $(MAKE) -C $(PROTOLIB)/makefiles -f Makefile.$(SYSTEM) clean; + +distclean: clean + +# DO NOT DELETE THIS LINE -- mkdep uses it. +# DO NOT PUT ANYTHING AFTER THIS LINE, IT WILL GO AWAY. diff --git a/norp/makefiles/Makefile.linux b/norp/makefiles/Makefile.linux new file mode 100644 index 0000000..bc2ec0b --- /dev/null +++ b/norp/makefiles/Makefile.linux @@ -0,0 +1,27 @@ +# +# Linux wxWindows Makefile definitions +# + +# 1) Where to find the wxWindows files +WX_FLAGS = `wx-config --cxxflags` +WX_LIBS = `wx-config --libs` + +# 2) System specific additional libraries, include paths, etc +# (Where to find X11 libraries, etc) +# +SYSTEM_FLAGS = +SYSTEM_INCLUDES = +SYSTEM_LIBS = -lm -lpthread +SYSTEM_REZ = touch + + +PROTO_CFLAGS = -DLINUX -DUNIX -DHAVE_IPV6 -DHAVE_GETLOGIN -D_FILE_OFFSET_BITS=64 -DHAVE_LOCKF -DHAVE_OLD_SIGNALHANDLER -DHAVE_DIRFD -DHAVE_ASSERT + +SYSTEM = linux +CC = g++ +RANLIB = ranlib +AR = ar + +SYSTEM_SRC = + +include Makefile.common diff --git a/norp/makefiles/Makefile.macosx b/norp/makefiles/Makefile.macosx new file mode 100644 index 0000000..81f1984 --- /dev/null +++ b/norp/makefiles/Makefile.macosx @@ -0,0 +1,48 @@ +# +# MacOS-X wxWindows Makefile definitions +# + +# 1) wxWidgets flags and libs +WX_CONFIG_PATH = /usr/local/bin/wx-config +WX_CFLAGS = `$(WX_CONFIG_PATH) --cxxflags` +WX_LIBS = `$(WX_CONFIG_PATH) --libs` + +# 2) System-specific flags and libs +SYSTEM_CFLAGS = -arch x86_64 +SYSTEM_LIBS = -arch x86_64 -lresolv + +# 3) System-specific Protolib flags (_must_ correspond to Protolib SYSTEM_HAVES) +# Must choose appropriate for the following: +# +# A) -DHAVE_CUSERID (preferred) or -DHAVE_GETLOGIN for cuserid() or getlogin() +# functions to obtain user's login name (We may change this to getpwd() +# if that is better across different platforms and login environments) +# +# B) -DHAVE_LOCKF (preferred) or -D_LOCKF for lockf() or flock() file locking +# functions to attempt exclusive lock on writing to files +# +# C) Specify -DHAVE_DIRFD if you system provides the "dirfd()" function +# (Most don't have it defined ... but some do) +# +# D) Optionally specify -DHAVE_ASSERT if your system has a built-in ASSERT() +# routine. +# +# E) Some systems (SOLARIS/SUNOS) have a few gotchas which require +# some #ifdefs to avoid compiler warnings ... so you might need +# to specify -DSOLARIS or -DSUNOS depending on your OS. +# (We export these for other Makefiles as needed) +# + +PROTO_CFLAGS = -DUNIX -DHAVE_IPV6 -DHAVE_ASSERT -DHAVE_GETLOGIN -DHAVE_FLOCK -DHAVE_DIRFD + +# Additional system-specific source code to use +SYSTEM_SRC = + +SYSTEM = macosx +CC = c++ +RANLIB = ranlib +AR = ar + +SYSTEM_BUNDLE = chant.app + +include Makefile.common diff --git a/norp/norm b/norp/norm new file mode 120000 index 0000000..a96aa0e --- /dev/null +++ b/norp/norm @@ -0,0 +1 @@ +.. \ No newline at end of file diff --git a/norp/src/common/norp.cpp b/norp/src/common/norp.cpp new file mode 100644 index 0000000..16ee4ba --- /dev/null +++ b/norp/src/common/norp.cpp @@ -0,0 +1,2704 @@ + +#include "norp.h" +#include "protoNet.h" +#include "protoDebug.h" + + +// Note this "PortPool" was a simple helper class +// for a test where we needed to traverse a +// NAT / firewall. We may _later_ refine this +// as an optional feature where "norp" can be +// configured to use a specific set of port +// numbers for NORM traffic. + +PortPool::PortPool(UINT16 basePort) + : base_port(basePort) +{ + memset(port_array, 0, 50*sizeof(bool)); +} + +UINT16 PortPool::GrabPort() +{ + for (UINT16 i = 0; i < 50; i++) + { + if (!port_array[i]) + { + port_array[i] = true; + return (base_port + i); + } + } + return 0; // no port was available +} // end PortPool::GrabPort() + +void PortPool::ReleasePort(UINT16 thePort) +{ + int index = thePort - base_port; + if ((index < 50) && (index >=0)) + port_array[index] = false; +} // end PortPool::ReleasePort() + +NorpMsg::NorpMsg(UINT32* bufferPtr, unsigned int numBytes, bool initFromBuffer, bool freeOnDestruct) + : ProtoPkt(bufferPtr, numBytes, freeOnDestruct) +{ + if (initFromBuffer) InitFromBuffer(numBytes); +} + +NorpMsg::~NorpMsg() +{ +} + +const double NorpSession::NORP_RTT_MIN = 1.0e-03; +const double NorpSession::NORP_RTT_MAX = 5.0; // TBD - assess these values (make runtime configurable?) +const double NorpSession::NORP_RTT_DEFAULT = 1.0;//e-01; // 100 msec default +const double Norp::DEFAULT_TX_RATE = 1.0e+06; +const double Norp::DEFAULT_PERSIST_INTERVAL = 60.0; // times out unsuccessful NORM delivery attempt after successive ACK_FAILURE + + +NorpSession::NorpSession(Norp& theController, UINT16 sessionId, NormNodeId originatorId) + : controller(theController), is_preset(false), socks_state(SOCKS_IDLE), + socks_client_socket(ProtoSocket::TCP), socks_remote_socket(ProtoSocket::TCP), + udp_relay_socket(ProtoSocket::UDP), + client_pending(0), client_index(0), remote_pending(0), remote_index(0), + norm_enable(true), norp_tx_socket(ProtoSocket::UDP), norp_rtt_estimate(NORP_RTT_DEFAULT), + norm_session(NORM_SESSION_INVALID), norm_tx_stream(NORM_OBJECT_INVALID), + norm_rx_stream(NORM_OBJECT_INVALID), norm_rx_pending(false), + persist_interval(Norp::DEFAULT_PERSIST_INTERVAL), persist_start_time(0, 0), + norm_rate_min(-1.0), norm_rate_max(-1.0), + norm_segment_size(0), norm_stream_buffer_max(0), norm_stream_buffer_count(0), + norm_stream_bytes_remain(0), norm_watermark_pending(false) +{ + memset(&session_id, 0, sizeof(Moniker)); + session_id.originator = originatorId; + session_id.identifier = sessionId; + socks_client_socket.SetNotifier(&controller.GetSocketNotifier()); + //socks_client_socket.SetListener(this, &NorpSession::OnSocksClientEvent); + socks_remote_socket.SetNotifier(&controller.GetSocketNotifier()); + socks_remote_socket.SetListener(this, &NorpSession::OnSocksRemoteEvent); + udp_relay_socket.SetNotifier(&controller.GetSocketNotifier()); + udp_relay_socket.SetListener(this, &NorpSession::OnUdpRelayEvent); + + norp_tx_socket.SetUserData(this); + norp_tx_socket.SetNotifier(&controller.GetSocketNotifier()); + norp_tx_socket.SetListener(&controller, &Norp::OnNorpSocketEvent); + + norp_msg_timer.SetListener(this, &NorpSession::OnNorpMsgTimeout); + norp_msg_timer.SetInterval(controller.GetInitialRtt()); + norp_msg_timer.SetRepeat(20); + + norp_msg.AttachBuffer(norp_msg_buffer, NORP_BUFFER_SIZE, false); + + close_timer.SetListener(this, &NorpSession::OnCloseTimeout); + close_timer.SetInterval(0.0); + close_timer.SetRepeat(0); // one-shot timer +} + +NorpSession::~NorpSession() +{ + socks_state = SOCKS_VOID; + Close(); +} + +void NorpSession::SetNormRateBounds(double rateMin, double rateMax) +{ + norm_rate_min = rateMin; + norm_rate_max = rateMax; + if (NORM_SESSION_INVALID != norm_session) + { + NormSetTxRateBounds(norm_session, rateMin, rateMax); + if (Norp::NORM_FIXED == controller.GetNormCC()) + { + // cumulative rate limit may override fixed session rate + // (i.e. to share pipe among multiple flows) + if ((rateMin >= 0.0) && (rateMin < controller.GetNormTxRate())) + NormSetTxRate(norm_session, rateMin); + else + NormSetTxRate(norm_session, controller.GetNormTxRate()); + } + } +} // end NorpSession::SetNormRateBounds() + +NormNodeId NorpSession::GetNodeId() const +{ + return controller.GetNormNodeId(); +} // end NorpSession::GetNodeId() + +void NorpSession::ActivateTimer(ProtoTimer& theTimer) +{ + controller.ActivateTimer(theTimer); +} // end NorpSession::ActivateTimer() + +bool NorpSession::AcceptClientConnection(ProtoSocket& serverSocket, bool normEnable) +{ + ASSERT(socks_client_socket.IsClosed()); + if (!serverSocket.Accept(&socks_client_socket)) + { + PLOG(PL_ERROR, "NorpSession::AcceptClientConnection() error: server socket accept failure\n"); + return false; + } + // Assume the same notifier (for now) + socks_client_socket.SetListener(this, &NorpSession::OnSocksClientEvent); + norm_enable = normEnable; + // Init our socks_state + socks_state = SOCKS_GET_AUTH_REQ; + // Init client/remote buffer indices + client_pending = client_index = 0; + remote_pending = remote_index = 0; + return true; +} // end NorpSession::AcceptClientConnection() + +bool NorpSession::AcceptPresetClientConnection(NorpPreset& preset, bool normEnable) +{ + ASSERT(socks_client_socket.IsClosed()); + if (!preset.AccessServerSocket().Accept(&socks_client_socket)) + { + PLOG(PL_ERROR, "NorpSession::AcceptPresetClientConnection() error: server socket accept failure\n"); + return false; + } + is_preset = true; + // Assume the same notifier (for now) + socks_client_socket.SetListener(this, &NorpSession::OnSocksClientEvent); + + norm_enable = normEnable; + // Jump-start our socks_state to + socks_state = SOCKS_GET_REQUEST; + // Init client/remote buffer indices + client_pending = client_index = 0; + remote_pending = remote_index = 0; + + if (norm_enable) + { + // Make and send SOCKS request to remote peer NORP server (peer will TCP connect to endpoint and reply via NORM) + ProtoPktSOCKS::Request request(client_buffer, 32, false); + request.SetVersion(5); + request.SetCommand(ProtoPktSOCKS::Request::CONNECT); + request.SetAddress(preset.GetDstAddr()); + return PutRemoteRequest(request, preset.GetNorpAddr()); + } + else + { + return ConnectToRemote(preset.GetDstAddr()); // do direct TCP connect to remote endpoint + } + + return true; +} // end NorpSession::AcceptPresetSession() + +void NorpSession::Shutdown() +{ + // Initiate hard session shutdown, informing remote + if (IsRemoteSession()) + { + norp_msg.Init(); + if (IsRemoteOriginator()) + norp_msg.SetType(NorpMsg::ORIG_END); + else + norp_msg.SetType(NorpMsg::CORR_END); + norp_msg.SetSessionId(GetSessionId()); + norp_msg.SetNodeId(controller.GetNormNodeId()); + // The remaining fields don't matter, so we don't send them! + norp_remote_addr.SetPort(controller.GetNorpPort()); + if (!controller.SendMessage(norp_msg, norp_remote_addr)) + PLOG(PL_ERROR, "NorpSession::Shutdown() warning: NORP message transmission failed (will try again)!\n"); + // Set timer to repeat until ACK_END is received + if (norp_msg_timer.IsActive()) norp_msg_timer.Deactivate(); + norp_msg_timer.SetInterval(2.0*norp_rtt_estimate); // use most recent RTT estimate + ActivateTimer(norp_msg_timer); + // Here we use ProtoSocket::Shutdown() (sockets get a DISCONNECT event upon shutdown completion) + // TBD - we may need to do something with input/output notification here? + if (socks_client_socket.IsOpen()) socks_client_socket.Close(); + if (socks_remote_socket.IsOpen()) socks_remote_socket.Close(); + socks_state = SOCKS_SHUTDOWN; + } + else + { + // No remote, so just do a hard Close() + Close(); + } +} // end NorpSession::Shutdown() + +// This returns "true" when the conditions of shutdown completion are met +bool NorpSession::ShutdownComplete() const +{ + if (socks_remote_socket.IsOpen()) return false; + if (socks_client_socket.IsOpen()) return false; + if (NORM_OBJECT_INVALID != norm_tx_stream) return false; + if (NORM_OBJECT_INVALID != norm_rx_stream) return false; + return true; +} // end NorpSession::ShutdownComplete() + +void NorpSession::Close() +{ + if (NORM_SESSION_INVALID != norm_session) + { + NormDestroySession(norm_session); + norm_session = NORM_SESSION_INVALID; + } + norp_tx_socket.Close(); + socks_client_socket.Close(); + socks_remote_socket.Close(); + udp_relay_socket.Close(); + client_pending = client_index = 0; + remote_pending = remote_index = 0; + if (SOCKS_VOID != socks_state) + { + // We make the state void here because controller deletes us! + socks_state = SOCKS_VOID; + controller.OnSessionClose(*this); + } +} // end NorpSession::Close() + +bool NorpSession::OnCloseTimeout(ProtoTimer& /*theTimer*/) +{ + close_timer.Deactivate(); + controller.OnSessionClose(*this); + return false; // because we've been deleted +} // end NorpSession::OnCloseTimeout() + + +void NorpSession::OnSocksClientEvent(ProtoSocket& theSocket, + ProtoSocket::Event theEvent) +{ + PLOG(PL_DETAIL, "NorpSession::OnSocksClientEvent("); + switch (theEvent) + { + case ProtoSocket::INVALID_EVENT: + PLOG(PL_DETAIL, "INVALID_EVENT) ...\n"); + break; + case ProtoSocket::CONNECT: + PLOG(PL_DETAIL, "CONNECT) ...\n"); + break; + case ProtoSocket::ACCEPT: + PLOG(PL_DETAIL, "ACCEPT) ...\n"); // should never happen (was accepted on server socket) unless remote BIND + break; + case ProtoSocket::SEND: + PLOG(PL_DETAIL, "SEND) ...\n"); + switch (socks_state) + { + case SOCKS_VOID: + case SOCKS_IDLE: + ASSERT(0); + break; + case SOCKS_GET_AUTH_REQ: + ASSERT(0); + break; + case SOCKS_PUT_AUTH_REP: + if (!PutClientAuthReply()) + { + PLOG(PL_ERROR, "NorpSession::OnSocksClientEvent() error: PutClientAuthReply() failure!\n"); + Close(); + return; + } + break; + case SOCKS_GET_REQUEST: + ASSERT(0); + break; + case SOCKS_PUT_REQUEST: + ASSERT(0); + break; + case SOCKS_CONNECTING: + ASSERT(0); + break; + case SOCKS_GET_REPLY: + ASSERT(0); + break; + case SOCKS_PUT_REPLY: + if (!PutClientReply()) + { + PLOG(PL_ERROR, "NorpSession::OnSocksClientEvent() error: PutClientReply() failure!\n"); + Shutdown(); + return; + } + break; + case SOCKS_CONNECTED: + if (!PutClientData()) + { + PLOG(PL_ERROR, "NorpSession::OnSocksClientEvent() error: PutClientData() failure!\n"); + Shutdown(); + return; + } + break; + case SOCKS_SHUTDOWN: + ASSERT(0); + break; + } // end switch(socks_state) + break; + case ProtoSocket::RECV: + { + PLOG(PL_DETAIL, "RECV) ...\n"); + switch (socks_state) + { + case SOCKS_VOID: + case SOCKS_IDLE: // initial, inactive state (socks_client_socket is closed) + ASSERT(0); + break; + case SOCKS_GET_AUTH_REQ: // first active state, getting auth request bytes from client + if (!GetClientAuthRequest()) + { + PLOG(PL_ERROR, "NorpSession::OnSocksClientEvent() error: GetClientAuthRequest() failure!\n"); + Close(); + return; + } + break; + case SOCKS_GET_REQUEST: // next, accumulate SOCKS request bytes from client + if (!GetClientRequest()) + { + PLOG(PL_ERROR, "NorpSession::OnSocksClientEvent() error: GetClientRequest() failure!\n"); + Close(); + return; + } + break; + case SOCKS_PUT_AUTH_REP: + case SOCKS_PUT_REQUEST: + case SOCKS_CONNECTING: + case SOCKS_PUT_REPLY: + case SOCKS_GET_REPLY: + case SOCKS_SHUTDOWN: + { + unsigned int numBytes = SOCKS_BUFFER_SIZE; + // do a recv() - probably means client disconnected (prematurely) + if (theSocket.Recv((char*)client_buffer, numBytes)) + { + if (SOCKS_SHUTDOWN != socks_state) + PLOG(PL_WARN, "NorpSession::OnSocksClientEvent() warning: unexpectedly received %u bytes from SOCKS client!\n", numBytes); + } + else + { + PLOG(PL_ERROR, "NorpSession::OnSocksClientEvent() error: unexpected receive error from SOCKS client!\n"); + } + break; + } + case SOCKS_CONNECTED: // finally, reading/writing data from/to client + if (0 == client_pending) // we need this because we sometimes get extra notifications with old "protolib" + { + if (!GetClientData()) + { + PLOG(PL_ERROR, "NorpSession::OnSocksClientEvent() error: GetClientData() failure!\n"); + Shutdown(); + } + } + break; + } // end switch(socks_state) + break; + } + case ProtoSocket::DISCONNECT: + PLOG(PL_DETAIL, "DISCONNECT) ...\n"); + switch (socks_state) + { + case SOCKS_VOID: + case SOCKS_IDLE: + ASSERT(0); // will never happen + break; + case SOCKS_GET_AUTH_REQ: + case SOCKS_PUT_AUTH_REP: + case SOCKS_GET_REQUEST: + case SOCKS_PUT_REQUEST: + case SOCKS_CONNECTING: + case SOCKS_PUT_REPLY: + case SOCKS_GET_REPLY: + PLOG(PL_ERROR, "NorpSession::OnSocksClientEvent() error: premature SOCKS client disconnect!\n"); + Shutdown(); // unexpected, premature client disconnect + break; + case SOCKS_CONNECTED: + // Client finished sending (and receiving) data + socks_state = SOCKS_SHUTDOWN; + if (0 == client_pending) + PutRemoteData(); // to shut down socks_remote_socket or norm_tx_stream + case SOCKS_SHUTDOWN: + theSocket.Close(); + if (ShutdownComplete()) + Close(); + // else more shutdown actions pending + break; + } + break; + + case ProtoSocket::EXCEPTION: + PLOG(PL_DETAIL, "EXCEPTION) ...\n"); + ASSERT(0); + break; + + case ProtoSocket::ERROR_: + PLOG(PL_DETAIL, "ERROR_) ...\n"); + break; + } // end switch (theEvent) +} // end NorpSession::OnSocksClientEvent() + +bool NorpSession::GetClientAuthRequest() +{ + // This method incrementally receives/processes a SOCKS AuthRequest + // and updates our "socks_state" and buffer indices accordlingly + // ("numBytes" is set to how many needed for next socket read) + ASSERT(SOCKS_GET_AUTH_REQ == socks_state); + for(;;) // loops until auth request is read or no more input is ready + { + switch (client_index) + { + case 0: // need to read VERSION byte first + { + client_pending = 1; + break; + } + case 1: // validate VERSION and read NMETHODS byte + { + ProtoPktSOCKS::AuthRequest authReq(client_buffer, 1); + if (5 != authReq.GetVersion()) + { + PLOG(PL_ERROR, "NorpSession::GetClientAuthRequest() received incompatible version %d request!\n", authReq.GetVersion()); + return false; + } + client_pending = 2; + break; + } + case 2: // Check NMETHODS byte to see how much to read. + { + ProtoPktSOCKS::AuthRequest authReq(client_buffer, 2); + UINT8 numBytes = authReq.GetMethodCount(); + if (0 == numBytes) + { + PLOG(PL_ERROR, "NorpSession::GetClientAuthRequest() received AuthRequest with zero methods!\n"); + return false; + } + client_pending += numBytes; + break; + } + default: + { + if (client_index == client_pending) + { + PLOG(PL_DETAIL, "NorpSession::GetClientAuthRequest() session %u received SOCKS AuthRequest from client %s/%hu ...\n", + GetSessionId(), socks_client_socket.GetDestination().GetHostString(), socks_client_socket.GetDestination().GetPort()); + // We have received the full SOCKS AuthRequest + // Find best authentication "method" (currently only AUTH_NONE) + ProtoPktSOCKS::AuthType authType = ProtoPktSOCKS::AUTH_INVALID; + ProtoPktSOCKS::AuthRequest authReq(client_buffer, client_index); + for (UINT8 i = 0; i < authReq.GetMethodCount(); i++) + { + if (ProtoPktSOCKS::AUTH_NONE == authReq.GetMethod(i)) + { + authType = ProtoPktSOCKS::AUTH_NONE; + break; + } + } + if (ProtoPktSOCKS::AUTH_NONE == authType) + { + // Build our SOCKS AuthReply into "remote_buffer" for transmission to client + socks_state = SOCKS_PUT_AUTH_REP; + ProtoPktSOCKS::AuthReply authReply(remote_buffer, SOCKS_BUFFER_SIZE, false); + authReply.SetVersion(5); + authReply.SetMethod(ProtoPktSOCKS::AUTH_NONE); + remote_pending = authReply.GetLength(); + remote_index = 0; + client_index = client_pending = 0; + // TBD - should we pause client input notification here? + return PutClientAuthReply(); + } + else + { + PLOG(PL_ERROR, "NorpSession::GetClientAuthRequest() received AuthRequest with no compatible method!\n"); + return false; + } + } + break; + } + } // end switch(client_rx_index) + + // Read next chunk of AuthRequest (as long as socket has data to read) + unsigned int numBytes = client_pending - client_index; + if (!socks_client_socket.Recv(((char*)client_buffer)+client_index, numBytes)) + { + PLOG(PL_ERROR, "NorpSession::OnSocksClientEvent() error: client socket recv() failure!\n"); + return false; + } + client_index += numBytes; + if (client_index < client_pending) break; // no more to read, so wait until RECV notification for more + } // end for(;;) + return true; +} // end NorpSession::GetClientAuthRequest() + +bool NorpSession::PutClientAuthReply() +{ + ASSERT(SOCKS_PUT_AUTH_REP == socks_state); + // We have a SOCKS AuthReply in our "remote_buffer" pending transmission to the SOCKS client + unsigned int numBytes = remote_pending - remote_index; + if (socks_client_socket.Send(((char*)remote_buffer)+remote_index, numBytes)) + { + remote_index += numBytes; + if (remote_index < remote_pending) + { + // Only partially sent, cue output notification to complete + socks_client_socket.StartOutputNotification(); + } + else + { + // AuthReply was fully sent + socks_client_socket.StopOutputNotification(); + socks_state = SOCKS_GET_REQUEST; + remote_pending = remote_index = 0; + client_pending = 1; // To get VERSION byte of impending SOCKS Request + client_index = 0; + } + return true; + } + else + { + PLOG(PL_ERROR, "NorpSession::PutClientAuthReply() error: auth reply send failure!\n"); + return false; + } +} // end NorpSession::PutClientAuthReply() + +bool NorpSession::GetClientRequest() +{ + ASSERT(SOCKS_GET_REQUEST == socks_state); + // SOCKS5 request: VERSION + CMD + RESV + ADDR_TYP + DST.ADDR + DST.PORT + // bytes: 1 + 1 + 1 + 1 + N + 2 + for(;;) // loops until client request is read or no more input is ready + { + switch (client_index) + { + case 0: // need to read VERSION byte first + { + client_pending = 1; + break; + } + case 1: // Validate VERSION, and get REQ, if applicable + { + ProtoPktSOCKS::Request req(client_buffer, 1); + if (5 != req.GetVersion()) + { + PLOG(PL_ERROR, "NorpSession::GetClientRequest() received incompatible version %d request!\n", req.GetVersion()); + return false; + } + client_pending = 4; // ask for bytes up through ADDR_TYP + break; + } + case 2: // Handle request based on command type + case 3: + { + // no change, just CMD and/or RESV byte coming in + ASSERT(4 == client_pending); + break; + } + case 4: // Process ADDR_TYP to see how many more to ask for + { + ProtoPktSOCKS::Request req(client_buffer, 4); + switch (req.GetAddressType()) + { + case ProtoPktSOCKS::IPv4: + client_pending = 4 + (4 + 2); // IPv4 addr + port + break; + case ProtoPktSOCKS::NAME: + client_pending = 4 + 1; // ask for name length byte + break; + case ProtoPktSOCKS::IPv6: + client_pending = 4 + (16 + 2); // IPv6 addr + port + break; + default: + PLOG(PL_ERROR, "NorpSession::GetClientRequest() error: invalid address type!\n"); + return false; + } + break; + } + case 5: // Process NAME length byte + { + ProtoPktSOCKS::Request req(client_buffer, 5); + if (ProtoPktSOCKS::NAME == req.GetAddressType()) + client_pending = 5 + (req.GetAddressLength() + 2); // DNS name + port + break; + } + default: + { + if (client_index == client_pending) + { + // Entire SOCKS Request from local client has been received, so handle it ... + ProtoPktSOCKS::Request request(client_buffer, client_index); + client_pending = client_index = 0; // finished with client buffer for moment after handing request here + return OnClientRequest(request, socks_client_socket.GetDestination()); + } + break; + } + } + // Read next chunk of client Request (as long as socket has data to read) + unsigned int numBytes = client_pending - client_index; + if (!socks_client_socket.Recv(((char*)client_buffer)+client_index, numBytes)) + { + PLOG(PL_ERROR, "NorpSession::OnSocksClientEvent() error: client socket recv() failure!\n"); + return false; + } + client_index += numBytes; + if (client_index < client_pending) break; // no more to read, so wait until RECV notification for more + } // end for(;;;) + return true; +} // end NorpSession::GetClientRequest() + + +bool NorpSession::OnClientRequest(const ProtoPktSOCKS::Request request, const ProtoAddress& srcAddr) +{ + ProtoAddress destAddr; + if (!request.GetAddress(destAddr)) + { + PLOG(PL_ERROR, "NorpSession::HandleSocksRequest() error: request with invalid destination address!\n"); + return false; + } + switch (request.GetCommand()) + { + case ProtoPktSOCKS::Request::CONNECT: + PLOG(PL_INFO, "norp: session %u recvd CONNECT request dst addr/port = %s/%hu\n", GetSessionId(), destAddr.GetHostString(), destAddr.GetPort()); + if (norm_enable) + { + // TBD - Determine "route" for "destAddr" to potentially separate remote "norp" peer + // (for now we assume "norp" remote is collocated on "destAddr" endpoints unless + // the controller has been configured for a specific remote 'correspondent') + ProtoAddress norpAddr; + controller.GetRemoteNorpAddress(destAddr, norpAddr); + // Relay SOCKS request to remote peer NORP server (peer will TCP connect to endpoint and reply via NORM) + return PutRemoteRequest(request, norpAddr); + } + else + { + return ConnectToRemote(destAddr); // do direct TCP connect to remote endpoint + } + case ProtoPktSOCKS::Request::BIND: + PLOG(PL_INFO, "norp: session %u recvd BIND request dst addr/port = %s/%hu\n", GetSessionId(), destAddr.GetHostString(), destAddr.GetPort()); + return BindRemote(destAddr.GetPort()); + case ProtoPktSOCKS::Request::UDP_ASSOC: + PLOG(PL_INFO, "norp: session %u recvd UDP_ASSOC for %s/%hu\n", GetSessionId(), destAddr.GetHostString(), destAddr.GetPort()); + // Save our UDP client's address (use the control socket addr if unspecified in request) + if (destAddr.IsUnspecified()) + { + udp_client_addr = srcAddr; + udp_client_addr.SetPort(destAddr.GetPort()); + } + else + { + udp_client_addr = destAddr; + } + return OpenUdpRelay(); + default: + PLOG(PL_ERROR, "NorpSession::OnClientRequest() error: invalid request command!\n"); + return false; + } +} // end NorpSession::OnClientRequest() + +bool NorpSession::PutRemoteRequest(const ProtoPktSOCKS::Request& request, const ProtoAddress& remoteAddr) +{ + // The "request" here sits in our "client_buffer". We know this buffer has room to append our + // "session_id.identifier" to the end. This "identifier" gives the command a unique key in + // the context of the NorpSession "originator" NormNodeId to recognize if it is a repeated + // request command, etc. + if (NORM_SESSION_INVALID == norm_session) + { + norm_session = NormCreateSession(controller.GetNormInstance(), remoteAddr.GetHostString(), controller.GetNormPort(), controller.GetNormNodeId()); + if (NORM_SESSION_INVALID == norm_session) + { + PLOG(PL_ERROR, "NorpSession::PutRemoteRequest() NormCreateSession() failure!\n"); + return false; + } + NormSetMessageTrace(norm_session, controller.GetNormTrace()); + NormSetUserData(norm_session, this); + NormSetDefaultSyncPolicy(norm_session, NORM_SYNC_STREAM); + + // We use a 2-step sender/receiver startup process by first opening an idle, "tx-only" NormSession to + // send the Request command to the remote "controller" NormSession (The remote will get our "tx port" + // info as part of this request sent). Our controller will get the REQ_ACK and/or SOCKS_REPLY message, + // then inform us, and so we will get the remote "tx port" info so we can then start our own receiver + // and "connect" our NORM rx_socket to the remote tx_socket. The sender here is made "idle" by disabling + // disabling the normal NORM GRTT probing. We don't re-activate that until we know the remote end has + // a receiver running (This is done upon REQ_ACK or SOCKS_REPLY reception when we also start our own NORM + // receiver). + + char ifaceName[64]; + ifaceName[63] = '\0'; + if (ProtoNet::GetInterfaceName(controller.GetProxyAddress(), ifaceName, 63)) + NormSetMulticastInterface(norm_session, ifaceName); + else + PLOG(PL_ERROR, "NorpSession::PutRemoteRequest() warning: unable to get interface name for address %s\n", controller.GetProxyAddress().GetHostString()); + switch (controller.GetNormCC()) + { + case Norp::NORM_CC: // default TCP-friendly congestion control + // do nothing, this is NORM's default mode + break; + case Norp::NORM_CCE: // "wireless-ready" ECN-only congestion control + NormSetEcnSupport(norm_session, true, true); + break; + case Norp::NORM_CCL: // "loss tolerant", non-ECN congestion control + NormSetEcnSupport(norm_session, false, false, true); + break; + case Norp::NORM_FIXED: // fixed-rate operation + // if cumulative rate limit has been imposed, it can override the fixed rate + // (i.e. to share pipe among multiple flows) + if ((norm_rate_min >= 0) && (norm_rate_min < controller.GetNormTxRate())) + NormSetTxRate(norm_session, norm_rate_min); + else + NormSetTxRate(norm_session, controller.GetNormTxRate()); + break; + } + if ((norm_rate_min >= 0.0) || (norm_rate_max >= 0.0)) + NormSetTxRateBounds(norm_session, norm_rate_min, norm_rate_max); + + // Even though we call "NormStartSender()" to get a "tx port" value, we disable GRTT probing and thus + // defer "real" sender startup until we get an ACK to our request (see "OriginatorStartNorm()") + NormSetTxOnly(norm_session, true); + NormSetGrttProbingMode(norm_session, NORM_PROBE_NONE); // no probing until remote receiver is started + if (!NormStartSender(norm_session, GetSessionId(), NORM_BUFFER_SIZE, controller.GetNormSegmentSize(), + controller.GetNormBlockSize(), controller.GetNormParityCount())) + { + PLOG(PL_ERROR, "NorpSession::PutRemoteRequest() error: NormStartSender() failure!\n"); + return false; + } + //NormSetTxSocketBuffer(norm_session, 4096); + NormSetFlowControl(norm_session, 0.0); // disable timer-based flow control since we are ACK-limiting writes to stream + norm_segment_size = controller.GetNormSegmentSize(); + norm_stream_buffer_max = ComputeNormStreamBufferSegmentCount(NORM_BUFFER_SIZE, norm_segment_size, controller.GetNormBlockSize()); + norm_stream_buffer_max -= controller.GetNormBlockSize(); // a little safety margin + norm_stream_buffer_count = 0; + norm_stream_bytes_remain = 0; + norm_watermark_pending = false; + + PLOG(PL_INFO, "norp: originator %lu created NORM session with dest %s/%hu (srcPort %hu)\n", controller.GetNormNodeId(), + remoteAddr.GetHostString(), controller.GetNormPort(), NormGetTxPort(norm_session)); + + // We open a UDP socket for NORP signaling here and "connect() it so we can get ICMP error messages + // (I.e., an ICMP "port unreachable" indicates the remote destination is not NORP-enabled) + if (!norp_tx_socket.Open()) + { + PLOG(PL_ERROR, "NorpSession::PutRemoteRequest() error: unable to open norp_tx_socket!\n"); + return false; + } + norp_remote_addr = remoteAddr; + norp_remote_addr.SetPort(controller.GetNorpPort()); + if (!norp_tx_socket.Connect(norp_remote_addr)) + { + PLOG(PL_ERROR, "NorpSession::PutRemoteRequest() error: unable to open and connect norp_tx_socket!\n"); + return false; + } + } + socks_client_socket.StopInputNotification(); // don't accept more from client until connected + PLOG(PL_INFO, "norp: node %u relaying SOCKS request to NORP destination %s/%hu\n", controller.GetNormNodeId(), + remoteAddr.GetHostString(), controller.GetNorpPort()); + // Put the SOCKS request into a NorpMsg and relay it to remote NORP server + // (Note that the request is "robustly" transmitted via periodic repetition until a request acknowledgment (REQ_ACK) is received + ASSERT(!norp_msg_timer.IsActive()); + norp_msg.SetType(NorpMsg::SOCKS_REQ); + norp_msg.SetSessionId(GetSessionId()); + norp_msg.SetSourcePort(NormGetTxPort(norm_session)); // Our NORM tx port number (so remote can "connect()" + norp_msg.SetDestinationPort(NormGetRxPort(norm_session)); // Our NORM rx port (for info) + norp_msg.SetNodeId(controller.GetNormNodeId()); // our NORM node id + norp_msg.SetContent((const char*)request.GetBuffer(), request.GetLength()); + + struct timeval currentTime; + ProtoSystemTime(currentTime); + norp_msg.SetTimestamp(currentTime); + if (!SendMessage(norp_msg)) + PLOG(PL_ERROR, "NorpSession::PutRemoteRequest() warning: NORP message transmission failed (will try again)!\n"); + norp_msg_timer.SetInterval(2.0 * norp_rtt_estimate); + ActivateTimer(norp_msg_timer); + socks_state = SOCKS_PUT_REQUEST; + return true; +} // end NorpSession::PutRemoteRequest() + +bool NorpSession::MakeDirectConnect() +{ + // This assumes our "norp_msg" previously sent in attempt to connect + // to a remote "norp" peer is cached (per PutRemoveRequest() + if (NORM_SESSION_INVALID != norm_session) + { + NormDestroySession(norm_session); + norm_session = NORM_SESSION_INVALID; + } + if (norp_msg_timer.IsActive()) norp_msg_timer.Deactivate(); + if (NorpMsg::SOCKS_REQ != norp_msg.GetType()) + { + PLOG(PL_ERROR, "NorpSession::MakeDirectConnect() error: invalid norp_msg enqueued!\n"); + return false; + } + + ProtoPktSOCKS::Request req(norp_msg.AccessContentPtr(), norp_msg.GetContentLength(), true); + ProtoAddress destAddr; + if (!req.GetAddress(destAddr)) + { + PLOG(PL_ERROR, "NorpSession::MakeDirectConnect() error: norp_msg SOCKS_REQ with invalid destination address enqueued!\n"); + return false; + } + return ConnectToRemote(destAddr); +} // end NorpSession::MakeDirectConnect() + +bool NorpSession::OnRemoteRequestAcknowledgment(const NorpMsg& theMsg, const ProtoAddress& senderAddr) +{ + // Note we update our RTT estimate on ACKs received + ProtoTime recvTime; + recvTime.GetCurrentTime(); + ProtoTime sentTime; + theMsg.GetTimestamp(sentTime); + if (SOCKS_PUT_REQUEST == socks_state) + { + norp_rtt_estimate = ProtoTime::Delta(recvTime, sentTime); // initial rtt received + if (!OriginatorStartNorm(senderAddr, theMsg.GetNodeId(), theMsg.GetSourcePort(), theMsg.GetDestinationPort())) + { + PLOG(PL_ERROR, "NorpSession::OnRemoteRequestAcknowledgment() error: OriginatorStartNormSender() failure!\n"); + return false; + } + // Note that successful OriginatorStartNorm() sets socks_state to SOCKS_CONNECTING + } + else + { + // Just use duplicative ACKs to update RTT estimate via EWMA + ASSERT(norp_rtt_estimate > 0.0); + double rttNew = ProtoTime::Delta(recvTime, sentTime); + norp_rtt_estimate = 0.5*(norp_rtt_estimate + rttNew); // TBD - is this a reasonable "smoothing" factor + } + // Note it's OK if a bona fide measured RTT is less than NORP_RTT_MIN + if (norp_rtt_estimate <= 0.0) + { + norp_rtt_estimate = NORP_RTT_MIN; + } + else if (norp_rtt_estimate > NORP_RTT_MAX) + { + PLOG(PL_WARN, "NorpSession::OnRemoteRequestAcknowledgment() warning: measured RTT exceeds max value of %lf sec.\n", NORP_RTT_MAX); + norp_rtt_estimate = NORP_RTT_MAX; + } + return true; +} // end NorpSession::OnRemoteRequestAcknowledgment() + +bool NorpSession::OnRemoteReply(const NorpMsg& theMsg, const ProtoAddress& senderAddr) +{ + ProtoPktSOCKS::Reply reply(theMsg.AccessContentPtr(), theMsg.GetContentLength()); + PLOG(PL_INFO, "NorpSession::OnRemoteReply() session %hu reply type %d\n", GetSessionId(), reply.GetType()); + if (ProtoPktSOCKS::Reply::SUCCESS == reply.GetType()) + { + if (SOCKS_PUT_REQUEST == socks_state) + { + // Somehow we missed the NORP REQUEST_ACK and the reply is being received first + // (TBD - should we init RTT here like above???) + if (!OriginatorStartNorm(senderAddr, theMsg.GetNodeId(), theMsg.GetSourcePort(), theMsg.GetDestinationPort())) + { + PLOG(PL_ERROR, "NorpSession::OnRemoteReply() error: OriginatorStartNormSender() failure!\n"); + return false; + } + // Note that successful OriginatorStartNorm() sets socks_state to SOCKS_CONNECTING + } + if (is_preset) + { + // Go straight to connected state and start + // accepting recv data from client or remote + client_index = client_pending = 0; + socks_client_socket.StartInputNotification(); + remote_index = remote_pending = 0; + socks_state = SOCKS_CONNECTED; + } + else if (SOCKS_CONNECTING == socks_state) + { + // TBD - we may want to put the reply in the client_buffer instead??? + // (i.e. in case we start receiving remote data before reply is fully sent?) + socks_state = SOCKS_PUT_REPLY; + memcpy(remote_buffer, (char*)reply.GetBuffer(), reply.GetLength()); + remote_pending = reply.GetLength(); + remote_index = 0; + PLOG(PL_INFO, "NorpSession::OnRemoteReply() originator relaying reply to SOCKS client ...\n"); + if (!PutClientReply()) + { + PLOG(PL_ERROR, "NorpSession::OnRemoteReply() error: unable to relay SOCKS reply to client!\n"); + return false; + } + } + // Send an ACK to the received reply + UINT32 buffer[32/4]; + NorpMsg ack(buffer, 32, false); + ack.SetType(NorpMsg::REP_ACK); + ack.SetSessionId(GetSessionId()); // our session id (note it was set by request originator) + ack.SetSourcePort(NormGetTxPort(norm_session)); // our NORM tx port number (so remote can "connect()" + ack.SetDestinationPort(NormGetRxPort(norm_session)); // our NORM rx port (for info) + struct timeval sentTime; + theMsg.GetTimestamp(sentTime); + ack.SetTimestamp(sentTime); // echo the sender's timestamp for their RTT measurement + ack.SetNodeId(controller.GetNormNodeId()); // our NORM node id + if (!controller.SendMessage(ack, senderAddr)) + PLOG(PL_ERROR, "NorpSession::OnRemoteReply() warning: unsable to transmit NORP REP_ACK message!\n"); + } + else + { + PLOG(PL_ERROR, "NorpSession::OnRemoteReply() received negative reply ...\n"); + return false; + } + return true; +} // end NorpSession::OnRemoteReply() + +// Note the "socks_state" MUST be SOCKS_PUT_REQUEST when this is invoked +// (starts associated NORM session upon receipt of request ack or reply) +bool NorpSession::OriginatorStartNorm(const ProtoAddress& senderAddr, + NormNodeId corrNormId, + UINT16 normSrcPort, + UINT16 normDstPort) +{ + ASSERT(SOCKS_PUT_REQUEST == socks_state); + if (norp_msg_timer.IsActive()) norp_msg_timer.Deactivate(); // cancels ongoing request retransmission, if applicable + // TBD - log the norp_msg_timer repeat count? + PLOG(PL_INFO, "norp: originator %u starting NORM sender probing (and receiver), connected to remote NORM source port %hu ...\n", + controller.GetNormNodeId(), normSrcPort); + NormSetGrttEstimate(norm_session, norp_rtt_estimate); // init NORM with value learned from NORP setup handshake + NormSetRxPortReuse(norm_session, true, NULL, senderAddr.GetHostString(), normSrcPort); + if (!NormStartReceiver(norm_session, NORM_BUFFER_SIZE)) + { + PLOG(PL_ERROR, "NorpSession::OriginatorStartNorm() error: NormStartReceiver() failure!\n"); + return false; + } + // The "NormChangeDestination()" call here makes sure our NORM session sends its packets + // to the destination that the remote correspondent server wants us to use for NORM + NormChangeDestination(norm_session, senderAddr.GetHostString(), normDstPort); + if (Norp::NORM_FIXED != controller.GetNormCC()) + NormSetCongestionControl(norm_session, true); // Note this also re-enables GRTT probing that was disabled at session creation + else + NormSetGrttProbingMode(norm_session, NORM_PROBE_ACTIVE); // re-enables previously suspended GRTT probing + if (NORM_OBJECT_INVALID == (norm_tx_stream = NormStreamOpen(norm_session, NORM_BUFFER_SIZE))) + { + PLOG(PL_ERROR, "NorpSession::OriginatorStartNorm() NormStreamOpen() failure!\n"); + return false; + } + if (!NormAddAckingNode(norm_session, corrNormId)) + { + PLOG(PL_ERROR, "NorpSession::OriginatorStartNorm() NormAddAckingNode() failure!\n"); + return false; + } + socks_state = SOCKS_CONNECTING; + return true; +} // end NorpSession::OriginatorStartNorm() + + +bool NorpSession::OnRemoteRequest(const NorpMsg& theMsg, const ProtoAddress& senderAddr) +{ + ProtoPktSOCKS::Request request(theMsg.AccessContentPtr(), theMsg.GetContentLength(), true); + PLOG(PL_INFO, "NorpSession::OnRemoteRequest() session %hu request type %d (source port %hu)\n", + GetSessionId(), request.GetCommand(), theMsg.GetSourcePort()); + if (NORM_SESSION_INVALID == norm_session) + { + // 0) Store the sender address to transmit REPLY to when connected to remote + norp_remote_addr = senderAddr; + // 1) Create a NormSession with request sender as session (destination) address + UINT16 normSessionPort = controller.GetNormPort(); + norm_session = NormCreateSession(controller.GetNormInstance(), senderAddr.GetHostString(), normSessionPort, controller.GetNormNodeId()); + if (NORM_SESSION_INVALID == norm_session) + { + PLOG(PL_ERROR, "NorpSession::OnRemoteRequest() NormCreateSession() failure!\n"); + return false; + } + NormSetMessageTrace(norm_session, controller.GetNormTrace()); + // Set the NormSession user data so NORM API events can be directed to this session + NormSetUserData(norm_session, this); + NormSetDefaultSyncPolicy(norm_session, NORM_SYNC_STREAM); + + // Start our NORM session as a receiver (we start the sender later when we get a REPLY_ACK, or NORM_DATA) + // (First, enable port reuse and "connect()" to the sender's NORM source addr/port) + char ifaceName[64]; + ifaceName[63] = '\0'; + if (ProtoNet::GetInterfaceName(controller.GetProxyAddress(), ifaceName, 63)) + NormSetMulticastInterface(norm_session, ifaceName); + else + PLOG(PL_ERROR, "NorpSession::OnRemoteRequest() warning: unable to get interface name for address %s\n", controller.GetProxyAddress().GetHostString()); + switch (controller.GetNormCC()) + { + case Norp::NORM_CC: // default TCP-friendly congestion control + // do nothing, this is NORM's default mode + break; + case Norp::NORM_CCE: // "wireless-ready" ECN-only congestion control + NormSetEcnSupport(norm_session, true, true); + break; + case Norp::NORM_CCL: // "loss tolerant", non-ECN congestion control + NormSetEcnSupport(norm_session, false, false, true); + break; + case Norp::NORM_FIXED: // fixed-rate transmission (no congestion control) + // if cumulative rate limit has been imposed, it can override the fixed rate + // (i.e. to share pipe among multiple flows) + if ((norm_rate_min >= 0) && (norm_rate_min < controller.GetNormTxRate())) + NormSetTxRate(norm_session, norm_rate_min); + else + NormSetTxRate(norm_session, controller.GetNormTxRate()); + break; + } + if ((norm_rate_min >= 0.0) || (norm_rate_max >= 0.0)) + NormSetTxRateBounds(norm_session, norm_rate_min, norm_rate_max); + // The "NormSetRxPortReuse()" call here "connects" us to the remote NORM sender source addr/port + // (this creates a proper binding since we reuse the same port number for multiple NORM sessions) + NormSetRxPortReuse(norm_session, true, NULL, senderAddr.GetHostString(), theMsg.GetSourcePort()); + if (!NormStartReceiver(norm_session, NORM_BUFFER_SIZE)) + { + PLOG(PL_ERROR, "NorpSession::OnRemoteRequest() error: NormStartReceiver() failure!\n"); + return false; + } + // This call makes sure we send our NORM packets to the originator's NORM port (i.e, if different than ours) + NormChangeDestination(norm_session, senderAddr.GetHostString(), theMsg.GetDestinationPort()); + if (!NormAddAckingNode(norm_session, theMsg.GetNodeId())) + { + PLOG(PL_ERROR, "NorpSession::OnRemoteRequest() NormAddAckingNode() failure!\n"); + return false; + } + PLOG(PL_INFO, "norp: correspondent %lu created NORM session with dest %s/%hu (srcPort %hu) connected to NORM source port %hu\n", + controller.GetNormNodeId(), senderAddr.GetHostString(), controller.GetNormPort(), NormGetTxPort(norm_session), theMsg.GetSourcePort()); + + // 2) Initiate TCP connection to indicated endpoint address/port (should we do this first before setting up NORM?) + ProtoAddress destAddr; + if (!request.GetAddress(destAddr)) + { + PLOG(PL_ERROR, "NorpSession::OnRemoteRequest() error: request with invalid address!\n"); + return false; + } + switch (request.GetCommand()) + { + case ProtoPktSOCKS::Request::CONNECT: + PLOG(PL_INFO, "norp: session %u recvd remote CONNECT request dst add/port = %s/%hu\n", GetSessionId(), destAddr.GetHostString(), destAddr.GetPort()); + if (!ConnectToRemote(destAddr)) + { + PLOG(PL_ERROR, "NorpSession::OnRemoteRequest() error: ConnectToRemote() failure\n"); + // TBD - send failure reply + return false; + } + break; + case ProtoPktSOCKS::Request::BIND: + PLOG(PL_ERROR, "NorpSession::OnRemoteRequest() error: remote SOCKS BIND not supported\n"); + // TBD - send negative reply + return false; + case ProtoPktSOCKS::Request::UDP_ASSOC: + PLOG(PL_ERROR, "NorpSession::OnRemoteRequest() error: remote SOCKS UDP_ASSOC not supported\n"); + // TBD - send negative reply + return false; + default: + PLOG(PL_ERROR, "NorpSession::OnRemoteRequest() error: invalid request command!\n"); + return false; + } + } // end if (NORM_SESSION_INVALID == norm_session) + else + { + // TBD - verify this request matches our existing session info + } + // Note the "ConnectToRemote()" call above transitioned us to SOCKS_CONNECTING state + if (SOCKS_CONNECTING == socks_state) + { + ASSERT(!norp_msg_timer.IsActive()); + // 3) Send a NormCmd request acknowledged (REQ_ACK) to "squelch" further request resends from remote + // (This is sent as a one-shot in response to NorpMsg::SOCKS_REQ messages) + UINT32 buffer[32/4]; + NorpMsg ack(buffer, 32, false); + ack.SetType(NorpMsg::REQ_ACK); + ack.SetSessionId(GetSessionId()); + ack.SetNodeId(controller.GetNormNodeId()); // our NORM node id + ack.SetSourcePort(NormGetTxPort(norm_session)); // Our NORM tx port number (so remote can "connect()" + ack.SetDestinationPort(NormGetRxPort(norm_session)); // Our NORM rx port (let's remote where to send NORM pkts) + struct timeval sentTime; + theMsg.GetTimestamp(sentTime); + ack.SetTimestamp(sentTime); + if (!controller.SendMessage(ack, senderAddr)) + PLOG(PL_ERROR, "NorpSession::OnRemoteRequest() warning: NORP REQ_ACK message transmission failed (will try again)!\n"); + } + else if (SOCKS_PUT_REPLY == socks_state) + { + // The reply already sent upon connect was evidently not + // received (nor the REQ_ACK message(s) that were sent) + // Note calling this also resets the reply robust retransmission process + if (!PutRemoteReply()) + { + PLOG(PL_ERROR, "NorpSession::OnRemoteRequest() error: unable to send SOCKS_REPLY\n"); + return false; + } + } + else if (SOCKS_CONNECTED == socks_state) + { + // Evidently a late arriving request, so we just ignore it + PLOG(PL_INFO, "NorpSession::OnRemoteRequest() late arriving remote SOCK_REQUEST\n"); + } + return true; +} // end NorpSession::OnRemoteRequest() + +bool NorpSession::PutRemoteReply() +{ + // Send SOCKS reply to remote NORP server via NORM command + ASSERT(IsRemoteSession()); + norp_msg.Init(); + norp_msg.SetType(NorpMsg::SOCKS_REP); + norp_msg.SetSessionId(GetSessionId()); // our session id (note was selected by request originator) + norp_msg.SetSourcePort(NormGetTxPort(norm_session)); // our NORM tx port number (so remote can "connect()" + norp_msg.SetDestinationPort(NormGetRxPort(norm_session)); // our NORM rx port (for info) + norp_msg.SetNodeId(controller.GetNormNodeId()); // our NORM node id + ProtoPktSOCKS::Reply reply(norp_msg.AccessContentPtr(), norp_msg.GetBufferLength() - norp_msg.GetLength(), false); + reply.SetVersion(5); + reply.SetType(ProtoPktSOCKS::Reply::SUCCESS); + reply.SetAddress(socks_remote_socket.GetSourceAddr()); + norp_msg.SetContentLength(reply.GetLength()); + + // If an existing ack or reply cmd is pending, we cancel it. This + // resets the robust transmission of the reply + if (norp_msg_timer.IsActive()) norp_msg_timer.Deactivate(); + struct timeval currentTime; + ProtoSystemTime(currentTime); + norp_msg.SetTimestamp(currentTime); + if (!controller.SendMessage(norp_msg, norp_remote_addr)) + PLOG(PL_ERROR, "NorpSession::OnRemoteRequest() warning: NORP REQ_ACK message transmission failed (will try again)!\n"); + norp_msg_timer.SetInterval(2.0 * norp_rtt_estimate); + ActivateTimer(norp_msg_timer); + socks_state = SOCKS_PUT_REPLY; + return true; +} // end NorpSession::PutRemoteReply() + +bool NorpSession::OnRemoteReplyAcknowledgment(const NorpMsg& theMsg) +{ + // Note we update our RTT estimate in either case + ProtoTime recvTime; + recvTime.GetCurrentTime(); + ProtoTime sentTime; + theMsg.GetTimestamp(sentTime); + + if (SOCKS_PUT_REPLY == socks_state) + { + // Initial rtt measurement + norp_rtt_estimate = ProtoTime::Delta(recvTime, sentTime); + PLOG(PL_INFO, "norp node %u starting sender probing (and sender) ...\n", controller.GetNormNodeId()); + if (Norp::NORM_FIXED != controller.GetNormCC()) + NormSetCongestionControl(norm_session, true); + if (!NormStartSender(norm_session, GetSessionId(), NORM_BUFFER_SIZE, controller.GetNormSegmentSize(), + controller.GetNormBlockSize(), controller.GetNormParityCount())) + { + PLOG(PL_ERROR, "NorpSession::OnRemoteReplyAcknowledgment() NormStartSender() failure!\n"); + return false; + } + //NormSetTxSocketBuffer(norm_session, 4096); + NormSetFlowControl(norm_session, 0.0); // disable timer-based flow control since we are ACK-limiting writes to stream + norm_segment_size = controller.GetNormSegmentSize(); + norm_stream_buffer_max = ComputeNormStreamBufferSegmentCount(NORM_BUFFER_SIZE, norm_segment_size, controller.GetNormBlockSize()); + norm_stream_buffer_max -= controller.GetNormBlockSize(); // a little safety margin + norm_stream_buffer_count = 0; + norm_stream_bytes_remain = 0; + norm_watermark_pending = false; + if (NORM_OBJECT_INVALID == (norm_tx_stream = NormStreamOpen(norm_session, NORM_BUFFER_SIZE))) + { + PLOG(PL_ERROR, "NorpSession::OnRemoteReplyAcknowledgment() NormStreamOpen() failure!\n"); + return false; + } + if (norp_msg_timer.IsActive()) norp_msg_timer.Deactivate(); // This cancels the robust (repeated) remote reply transmission + socks_remote_socket.StartInputNotification(); // enable flow of data from remote TCP socket + socks_state = SOCKS_CONNECTED; + remote_index = remote_pending = 0; + } + else + { + ASSERT(norp_rtt_estimate > 0.0); + // Only use duplicative ACKs to update RTT estimate via EWMA + double rttNew = ProtoTime::Delta(recvTime, sentTime); + norp_rtt_estimate = 0.5*(norp_rtt_estimate + rttNew); // TBD - is this a reasonable "smoothing" factor + } + // Note it's OK if a bona fide measured RTT is less than NORP_RTT_MIN + if (norp_rtt_estimate <= 0.0) + { + norp_rtt_estimate = NORP_RTT_MIN; + } + else if (norp_rtt_estimate > NORP_RTT_MAX) + { + PLOG(PL_WARN, "NorpSession::OnRemoteReplyAcknowledgment() warning: measured RTT exceeds max value of %lf sec.\n", NORP_RTT_MAX); + norp_rtt_estimate = NORP_RTT_MAX; + } + return true; +} // end NorpSession::OnRemoteReplyAcknowledgment() + +bool NorpSession::ConnectToRemote(const ProtoAddress& destAddr) +{ + // Make a direct TCP connection to remote destAddr + ASSERT(socks_remote_socket.IsClosed()); + if (!socks_remote_socket.Connect(destAddr)) + { + PLOG(PL_ERROR, "NorpSession::ConnectToRemote() error: remote socket connect() failure!\n"); + return false; + } + ASSERT(!socks_remote_socket.IsConnected()); // TBD - handle immediate connection case??? + PLOG(PL_DETAIL, "NorpSession::ConnectToRemote() connection to %s/%hu initiated ...\n", + destAddr.GetHostString(), destAddr.GetPort()); + if (socks_client_socket.IsOpen()) + socks_client_socket.StopInputNotification(); // don't accept more from client until connected + socks_state = SOCKS_CONNECTING; + return true; +} // end NorpSession::ConnectToRemote() + +bool NorpSession::BindRemote(UINT16 bindPort) +{ + // Open our remote socket in unbound state + if (!socks_remote_socket.Open(0, proxy_addr.GetType(), false)) + { + PLOG(PL_ERROR, "NorpSession::BindRemote() error: unable to open socket for remote connection\n"); + return false; + } + // Typically, the BIND request should be for port zero (any available port) + // But, in the case that it is not, we set port reuse for when the norp server + // is set up as a "local, loopback" server + if (0 != bindPort) socks_remote_socket.SetReuse(true); + bool result = socks_remote_socket.Bind(bindPort, &proxy_addr); + if (!result) result = socks_remote_socket.Bind(0, &proxy_addr); + if (!result) + { + PLOG(PL_ERROR, "NorpSession::BindRemote() error: unable to bind for remote connection\n"); + return false; + } + if (!socks_remote_socket.Listen()) + { + PLOG(PL_ERROR, "NorpSession::BindRemote() error: unable to listen for remote connection\n"); + return false; + } + // Construct the first reply to inform client of bind addr + ProtoPktSOCKS::Reply reply(remote_buffer, SOCKS_BUFFER_SIZE, false); + reply.SetVersion(5); + reply.SetType(ProtoPktSOCKS::Reply::SUCCESS); + ProtoAddress bindAddr = proxy_addr; + bindAddr.SetPort(socks_remote_socket.GetPort()); + PLOG(PL_INFO,"norp proxy listening on addr/port = %s/%hu\n", bindAddr.GetHostString(), bindAddr.GetPort()); + reply.SetAddress(bindAddr); + remote_pending = reply.GetLength(); + remote_index = 0; + socks_state = SOCKS_PUT_REPLY; + return PutClientReply(); +} // NorpSession::BindRemote() + +bool NorpSession::OpenUdpRelay() +{ + // 1) open udp_relay_socket + if (!udp_relay_socket.Open()) + { + PLOG(PL_ERROR, "NorpSession::OpenUdpRelay() error: udp_relay_socket open failed!\n"); + return false; + } + + // 2) Build the SOCKS reply into the remote_buffer ... + ProtoPktSOCKS::Reply reply(remote_buffer, SOCKS_BUFFER_SIZE, false); + reply.SetVersion(5); + reply.SetType(ProtoPktSOCKS::Reply::SUCCESS); + // We use the bind (local source) addr of our client socket as the SOCKS reply BIND.ADDR + // and the port number of our udp relay socket as the SOCKS reply BIND.PORT + ProtoAddress bindAddr = socks_client_socket.GetSourceAddr(); + PLOG(PL_INFO, "udp relay bind addr = %s/%hu\n", bindAddr.GetHostString(), bindAddr.GetPort()); + bindAddr.SetPort(udp_relay_socket.GetPort()); + reply.SetAddress(bindAddr); + remote_pending = reply.GetLength(); + remote_index = 0; + socks_state = SOCKS_PUT_REPLY; + return PutClientReply(); + +} // end NorpSession::OpenUdpRelay() + +bool NorpSession::PutClientReply() +{ + // Transmit SOCKS reply that's in "remote_buffer" to local SOCKS client + // (This buffering and indexing allows for possibly multiple TCP sockets + // writes to send the reply, even though that is not very likely given a + // SOCKS reply is smaller than the minimum IP MTU of 512 bytes) + ASSERT(SOCKS_PUT_REPLY == socks_state); + unsigned int numBytes = remote_pending - remote_index; + if (socks_client_socket.Send(((char*)remote_buffer)+remote_index, numBytes)) + { + remote_index += numBytes; + if (remote_index < remote_pending) + { + // Still more to send, keep output notification going + socks_client_socket.StartOutputNotification(); + } + else + { + // Reply fully sent, now transition to SOCKS_CONNECTED state + PLOG(PL_DETAIL, "NorpSession::PutClientReply() session %hu:%08x SOCKS reply sent to client ...\n", + GetSessionId(), GetOriginatorId()); + remote_index = remote_pending = 0; + socks_client_socket.StopOutputNotification(); + // Start accepting recv data from client or remote + client_index = client_pending = 0; + socks_client_socket.StartInputNotification(); + if (socks_remote_socket.IsOpen()) + socks_remote_socket.StartInputNotification(); + socks_state = SOCKS_CONNECTED; + } + } + else + { + PLOG(PL_ERROR, "NorpSession::PutClientReply() error: remote socket send failure\n"); + return false; + } + return true; +} // end NorpSession::PutClientReply() + +bool NorpSession::GetClientData() +{ + if (0 != client_pending) return true; + // Get whatever data is ready from client and start sending to remote + unsigned int numBytes = SOCKS_BUFFER_SIZE; + if (IsRemoteSession() && !IsRemoteOriginator()) + { + // We're the remote correspondent, so get client data from remote originator (via NORM) + if (norm_rx_pending) + { + ASSERT(0 == client_pending); + if (NormStreamRead(norm_rx_stream, (char*)client_buffer, &numBytes)) + { + client_pending = numBytes; + client_index = 0; + if (numBytes < SOCKS_BUFFER_SIZE) norm_rx_pending = false; + PLOG(PL_DETAIL, "NorpSession::GetClientData() correspondent read %lu bytes from NORM stream ..\n", numBytes); + if (0 != numBytes) + return PutRemoteData(); + // else wait for NORM_RX_OBJECT_UPDATED notification + } + else + { + PLOG(PL_ERROR, "NorpSession::GetClientData() NormStreamRead() error: break in stream!\n"); + return false; + } + } + // else wait for NORM_RX_OBJECT_UPDATED notification + } + else + { + // Originator server so get data from SOCKS client via TCP + ASSERT(0 == client_pending); + if (socks_client_socket.Recv((char*)client_buffer, numBytes)) + { + socks_client_socket.StopInputNotification(); + client_pending = numBytes; + client_index = 0; + PLOG(PL_DETAIL, "NorpSession::GetClientData() originator read %lu bytes from SOCKS client ...\n", numBytes); + if (0 != numBytes) + return PutRemoteData(); + // else socket will disconnect itself + } + else + { + PLOG(PL_ERROR, "NorpSession::GetClientData() client socket recv failure!\n"); + return false; + } + } + return true; +} // end NorpSession::GetClientData() + +bool NorpSession::PutClientData() +{ + // Transmit data that's in "remote_buffer" to client + unsigned int numBytes = remote_pending - remote_index; + if (IsRemoteSession() && !IsRemoteOriginator()) + { + if (0 != numBytes) + { + numBytes = WriteToNormStream(((char*)remote_buffer)+remote_index, numBytes); + } + PLOG(PL_DETAIL, "correspondent wrote %u bytes to NORM stream ...\n", numBytes); + remote_index += numBytes; + if (remote_index == remote_pending) + { + // All pending client data has been enqueued to remote + remote_index = remote_pending = 0; + if (SOCKS_CONNECTED == socks_state) + { + FlushNormStream(false, NORM_FLUSH_ACTIVE); + socks_remote_socket.StartInputNotification(); + } + else + { + ASSERT(SOCKS_SHUTDOWN == socks_state); + // No more data to put, so gracefully close norm_tx_stream + NormStreamClose(norm_tx_stream, true); // Note our "trick" here to do a graceful close, _then_ watermark + NormSetWatermark(norm_session, norm_tx_stream, true); // future NORM API will add "bool watermark" option to graceful close + } + } + // else wait for NORM_TX_QUEUE_VACANCY / EMPTY or watermark acknowledgemnt completion + } + else + { + // Relay data to local SOCKS client via TCP + if (0 != numBytes) + { + if (!socks_client_socket.Send(((char*)remote_buffer)+remote_index, numBytes)) + { + if (SOCKS_CONNECTED == socks_state) + { + PLOG(PL_ERROR, "NorpSession::PutClientData() error: remote socket send failure\n"); + return false; + } + else // SOCKS_SHUTDOWN == socks_state + { + PLOG(PL_WARN, "NorpSession::PutClientData() warning: remote socket send failure\n"); + return true; + } + } + } + PLOG(PL_DETAIL, "originator wrote %lu bytes to client TCP socket ...\n", numBytes); + remote_index += numBytes; + if (remote_index < remote_pending) + { + // Still more data to send, keep output notification going + socks_client_socket.StartOutputNotification(); + } + else + { + // Data fully sent, re-enable remote_socket input notifcation + remote_index = remote_pending = 0; + socks_client_socket.StopOutputNotification(); + if (SOCKS_CONNECTED == socks_state) + { + if (IsRemoteSession()) + { + // We're the remote session originator, so get more data if ready + if (norm_rx_pending) + return GetRemoteData(); + // else wait for NORM_RX_OBJECT_UPDATED notification + } + else + { + // Restart accepting recv data from remote + socks_remote_socket.StartInputNotification(); + } + } + else // SOCKS_SHUTDOWN == socks_state + { + if (socks_client_socket.IsConnected()) + { + socks_client_socket.Shutdown(); + } + else + { + socks_client_socket.Close(); + if (ShutdownComplete()) Shutdown(); + } + } + } + } + return true; +} // end NorpSession::PutClientData() + +void NorpSession::OnSocksRemoteEvent(ProtoSocket& theSocket, + ProtoSocket::Event theEvent) +{ + PLOG(PL_DETAIL, "NorpSession::OnSocksRemoteEvent("); + switch (theEvent) + { + case ProtoSocket::INVALID_EVENT: + { + // should never happen, just for completeness of switch cases + PLOG(PL_DETAIL, "INVALID_EVENT) ...\n"); + break; + } + case ProtoSocket::CONNECT: + { + PLOG(PL_DETAIL, "CONNECT) ...\n"); + ASSERT(SOCKS_CONNECTING == socks_state); + PLOG(PL_DETAIL, "NorpSession::OnSocksRemoteEvent(CONNECT) session %hu:%08x connected to destination %s/%hu ...\n", + GetSessionId(), GetOriginatorId(), + socks_remote_socket.GetDestination().GetHostString(), + socks_remote_socket.GetDestination().GetPort()); + if (is_preset) + { + // Go straight to connected state and start + // accepting recv data from client or remote + client_index = client_pending = 0; + socks_client_socket.StartInputNotification(); + remote_index = remote_pending = 0; + socks_state = SOCKS_CONNECTED; + } + else + { + // Don't get data from remote until reply sent to client + socks_remote_socket.StopInputNotification(); + socks_state = SOCKS_PUT_REPLY; + if (IsRemoteSession()) + { + if (!PutRemoteReply()) + { + PLOG(PL_ERROR, "NorpSession::OnSocksRemoteEvent(CONNECT) error: SOCKS CONNECT remote reply send failure!\n"); + Shutdown(); + } + } + else + { + // Build reply in "remote_buffer" to send to client + ProtoPktSOCKS::Reply reply(remote_buffer, SOCKS_BUFFER_SIZE, false); + reply.SetVersion(5); + reply.SetType(ProtoPktSOCKS::Reply::SUCCESS); + reply.SetAddress(socks_remote_socket.GetSourceAddr()); + remote_pending = reply.GetLength(); + remote_index = 0; + // Send SOCKS reply to locally connected client + if (!PutClientReply()) + { + PLOG(PL_ERROR, "NorpSession::OnSocksRemoteEvent(CONNECT) error: SOCKS CONNECT local reply send failure!\n"); + Shutdown(); + } + } + } + break; + } + case ProtoSocket::ACCEPT: + { + PLOG(PL_DETAIL, "ACCEPT) ...\n"); + // This will happen for a listening socket put in place for BIND request + // (we need to send a second reply indicating the remote connection accepted) + ASSERT(SOCKS_CONNECTED == socks_state); + if (!socks_remote_socket.Accept()) + { + PLOG(PL_ERROR, "NorpSession::OnSocksRemoteEvent(ACCEPT) error: SOCKS BIND accept failure!\n"); + Shutdown(); + break; + } + // TBD - should check that the accepted remote dst addr/port is what it's supposed to be + ProtoPktSOCKS::Reply reply(remote_buffer, SOCKS_BUFFER_SIZE, false); + reply.SetVersion(5); + reply.SetType(ProtoPktSOCKS::Reply::SUCCESS); + reply.SetAddress(socks_remote_socket.GetDestination()); + remote_pending = reply.GetLength(); + remote_index = 0; + socks_state = SOCKS_PUT_REPLY; + if (!PutClientReply()) + { + PLOG(PL_ERROR, "NorpSession::OnSocksRemoteEvent(ACCEPT) error: SOCKS BIND reply send failure!\n"); + Shutdown(); + } + break; + } + case ProtoSocket::SEND: + PLOG(PL_DETAIL, "SEND) ...\n"); + // write pending data from client_buffer to remote + if (!PutRemoteData()) + { + PLOG(PL_ERROR, "NorpSession::OnSocksRemoteEvent() error: PutRemoteData() failure!\n"); + Shutdown(); + } + break; + case ProtoSocket::RECV: + { + PLOG(PL_DETAIL, "RECV) ...\n"); + if (0 == remote_pending) // we need this check because of occasional extra notifications with old "protolib" + { + if (!GetRemoteData()) + { + PLOG(PL_ERROR, "NorpSession::OnSocksRemoteEvent(SEND) error: GetRemoteData() failure!\n"); + Shutdown(); + } + } + break; + } + case ProtoSocket::DISCONNECT: + PLOG(PL_DETAIL, "DISCONNECT) ...\n"); + switch (socks_state) + { + case SOCKS_VOID: + case SOCKS_IDLE: + case SOCKS_GET_AUTH_REQ: + case SOCKS_PUT_AUTH_REP: + case SOCKS_GET_REQUEST: + case SOCKS_PUT_REQUEST: + ASSERT(0); + break; + case SOCKS_CONNECTING: + case SOCKS_PUT_REPLY: + case SOCKS_GET_REPLY: + PLOG(PL_ERROR, "NorpSession::OnSocksRemoteEvent() error: premature SOCKS client disconnect!\n"); + Shutdown(); // unexpected, premature client disconnect + break; + case SOCKS_CONNECTED: + socks_state = SOCKS_SHUTDOWN; + if (0 == remote_pending) + PutClientData(); // to shutdown NORM stream or socks_client_socket + case SOCKS_SHUTDOWN: + // Client finished sending (and receiving) data + theSocket.Close(); + if (ShutdownComplete()) + Close(); + // else further shutdown actions pending + break; + } + break; + + case ProtoSocket::EXCEPTION: + PLOG(PL_DETAIL, "EXCEPTION) ...\n"); // shouldn't happen + break; + + case ProtoSocket::ERROR_: + PLOG(PL_DETAIL, "ERROR_) ...\n"); + Shutdown(); + break; + } +} // end NorpSession::OnSocksRemoteEvent() + +bool NorpSession::GetRemoteData() +{ + // Receive data from remote socket into remote_buffer + // Get some data from remote and start sending it to the client + ASSERT(0 == remote_pending); + unsigned int numBytes = SOCKS_BUFFER_SIZE; + if (IsRemoteSession() && IsRemoteOriginator()) + { + // We're the remote correspondent (i.e. not the remote session originator) + if (norm_rx_pending) + { + ASSERT(0 == remote_pending); + if (NormStreamRead(norm_rx_stream, (char*)remote_buffer, &numBytes)) + { + remote_pending = numBytes; + remote_index = 0; + if (numBytes < SOCKS_BUFFER_SIZE) norm_rx_pending = false; + if (0 != numBytes) + return PutClientData(); + // else wait for NORM_RX_OBJECT_UPDATED notification + } + else + { + PLOG(PL_ERROR, "NorpSession::GetRemoteData() NormStreamRead() error: break in stream!\n"); + return false; + } + } + // else wait for NORM_RX_OBJECT_UPDATED notification + } + else + { + // Remote correspondent or direct-connect, so use TCP + ASSERT(0 == remote_pending); + if (socks_remote_socket.Recv((char*)remote_buffer, numBytes)) + { + remote_pending = numBytes; + remote_index = 0; + socks_remote_socket.StopInputNotification(); + if (0 != numBytes) + return PutClientData(); + // else socket will disconnect itself and session will close + } + else + { + PLOG(PL_ERROR, "NorpSession::GetRemoteData() error: remote socket recv failure!\n"); + return false; + } + } + return true; +} // end NorpSession::GetRemoteData() + +bool NorpSession::PutRemoteData() +{ + // Transmit data in "client_buffer" to remote + unsigned int numBytes = client_pending - client_index; + if (IsRemoteSession() && IsRemoteOriginator()) + { + if (0 != numBytes) + numBytes = WriteToNormStream(((char*)client_buffer)+client_index, numBytes); + PLOG(PL_DETAIL, "originator wrote %u bytes to NORM stream ...\n", numBytes); + client_index += numBytes; + if (client_index == client_pending) + { + // All pending client data has been enqueued to remote + client_index = client_pending = 0; + if (SOCKS_CONNECTED == socks_state) + { + FlushNormStream(false, NORM_FLUSH_ACTIVE); + socks_client_socket.StartInputNotification(); // to get more data + } + else + { + ASSERT(SOCKS_SHUTDOWN == socks_state); + // gracefully close our tx stream + NormStreamClose(norm_tx_stream, true); // Note our "trick" here to do a graceful close, _then_ watermark + NormSetWatermark(norm_session, norm_tx_stream, true); // future NORM API will add "bool watermark" option to graceful close + } + } + // else wait for NORM_TX_QUEUE_VACANCY / EMPTY or watermark acknowledgment completion + } + else + { + // Use TCP to send data directly to remote endpoint + if (0 != numBytes) + { + if (!socks_remote_socket.Send(((char*)client_buffer)+client_index, numBytes)) + { + PLOG(PL_ERROR, "NorpSession::PutRemoteData() error: remote TCP socket send failure\n"); + return false; + } + } + PLOG(PL_DETAIL, "%s wrote %lu bytes to remote TCP socket ...\n", IsRemoteSession() ? "correspondent" : "server", numBytes); + client_index += numBytes; + if (client_index < client_pending) + { + socks_remote_socket.StartOutputNotification(); + } + else + { + // All pending client data has been sent to remote + socks_remote_socket.StopOutputNotification(); + client_index = client_pending = 0; + if (SOCKS_CONNECTED == socks_state) + { + if (IsRemoteSession()) + { + // We're the remote correspondent (i.e. not the remote session originator) + if (norm_rx_pending) + return GetClientData(); + // else wait for NORM_RX_OBJECT_UPDATED notification + } + else + { + socks_client_socket.StartInputNotification(); // prompt for more from client + } + } + else + { + ASSERT(SOCKS_SHUTDOWN == socks_state); + if (socks_remote_socket.IsConnected()) + { + socks_remote_socket.Shutdown(); + } + else + { + socks_remote_socket.Close(); + if (ShutdownComplete()) Shutdown(); + } + } + } + } + return true; +} // end NorpSession::PutRemoteData() + + +void NorpSession::OnUdpRelayEvent(ProtoSocket& theSocket, + ProtoSocket::Event theEvent) +{ + switch (theEvent) + { + case ProtoSocket::RECV: + { + UINT32 buffer[8192/4]; + ProtoAddress srcAddr; + char* bufPtr = ((char*)buffer) + 22; // allows room for IPv6 header to be applied + unsigned int numBytes = (8192 - 22); + while (udp_relay_socket.RecvFrom(bufPtr, numBytes, srcAddr)) + { + if (0 == numBytes) break; // no more data to read + if (srcAddr.IsEqual(udp_client_addr)) + { + + // Packet is from client, so relay it ... + // (First need to parse SOCKS header to get destination, etc + // The cast up to UINT32* here is safe because UdpRequest uses byte boundaries + ProtoPktSOCKS::UdpRequest udpReq((UINT32*)bufPtr, numBytes); + if (0 != udpReq.GetFrag()) + { + PLOG(PL_ERROR, "NorpSession::OnUdpRelayEvent() received fragmented packet from client!" + " (fragmentation not yet supported.)\n"); + numBytes = (8192 - 22); + continue; // get next packet + } + ProtoAddress dstAddr; + if (!udpReq.GetAddress(dstAddr)) + { + PLOG(PL_ERROR, "NorpSession::OnUdpRelayEvent() received packet with bad destination address from client!\n"); + numBytes = 8192; + continue; // get next packet + } + // Relay the packet (TBD - add buffering, etc?) + if (!udp_relay_socket.SendTo(udpReq.GetDataPtr(), udpReq.GetDataLength(), dstAddr)) + { + PLOG(PL_ERROR, "NorpSession::OnUdpRelayEvent() error: unable to send packet to %s/%hu!\n", + dstAddr.GetHostString(), dstAddr.GetPort()); + numBytes = (8192 - 22); + continue; // get next packet + } + } + else + { + // TBD - should we make sure the dest addr was for our client? + // Relay inbound packet to SOCKS client + // Note we left room for our SOCKS UdpRequest header to be applied in front of payload data + unsigned int headerOffset = 0; + if (ProtoAddress::IPv4 == srcAddr.GetType()) + headerOffset = 12; // IPv4 addr is 12 bytes shorter than IPv6 + else if (ProtoAddress::IPv6 != srcAddr.GetType()) + ASSERT(0); // can only receive IPv4 or IPv6 packets1 + char* reqPtr = ((char*)buffer) + headerOffset; + // The cast up to UINT32* here is safe because UdpRequest uses byte boundaries + ProtoPktSOCKS::UdpRequest udpReq((UINT32*)reqPtr, numBytes + 22 - headerOffset, false); + udpReq.SetFragment(0); + udpReq.SetAddress(srcAddr); + udpReq.SetDataLength(numBytes); + // Send to client + if (!udp_relay_socket.SendTo(reqPtr, udpReq.GetLength(), udp_client_addr)) + { + PLOG(PL_ERROR, "NorpSession::OnUdpRelayEvent() error: unable to relay inbound packet to %s/%hu!\n", + udp_client_addr.GetHostString(), udp_client_addr.GetPort()); + numBytes = (8192 - 22); + continue; // get next packet + } + } + numBytes = (8192 - 22); // reset "numBytes" + } + break; + } + default: + PLOG(PL_DETAIL, "NorpSession::OnUdpRelayEvent() unhandled event type %d\n", theEvent); + break; + } // end switch(theEvent) + +} // end NorpSession::OnUdpRelayEvent() + +bool NorpSession::GetNormNodeAddress(NormNodeHandle nodeHandle, ProtoAddress& theAddr) +{ + char addrBuffer[16]; + unsigned int addrLen = 16; + UINT16 portNum; + if (NormNodeGetAddress(nodeHandle, addrBuffer, &addrLen, &portNum)) + { + if (4 == addrLen) + theAddr.SetRawHostAddress(ProtoAddress::IPv4, addrBuffer, 4); + else + theAddr.SetRawHostAddress(ProtoAddress::IPv6, addrBuffer, 16); + theAddr.SetPort(portNum); + return true; + } + else + { + PLOG(PL_ERROR, "NorpSession::GetNormNodeAddress() error: NormNodeGetAddress() failed!\n"); + theAddr.Invalidate(); + return false; + } +} // end NorpSession::GetNormNodeAddress() + +unsigned int NorpSession::ComputeNormStreamBufferSegmentCount(unsigned int bufferBytes, UINT16 segmentSize, UINT16 blockSize) +{ + // This same computation is performed in NormStreamObject::Open() in "normObject.cpp" + unsigned int numBlocks = bufferBytes / (blockSize * segmentSize); + if (numBlocks < 2) numBlocks = 2; // NORM enforces a 2-block minimum buffer size + return (numBlocks * blockSize); +} // end NorpSession::ComputeNormStreamBufferSegmentCount() + +unsigned int NorpSession::WriteToNormStream(const char* buffer, + unsigned int numBytes) +{ + // This method uses NormStreamWrite(), but limits writes by explicit ACK-based flow control status + if (norm_stream_buffer_count < norm_stream_buffer_max) + { + // 1) How many buffer bytes are available? + unsigned int bytesAvailable = norm_segment_size * (norm_stream_buffer_max - norm_stream_buffer_count); + bytesAvailable -= norm_stream_bytes_remain; // unflushed segment portiomn + if (numBytes <= bytesAvailable) + { + unsigned int totalBytes = numBytes + norm_stream_bytes_remain; + unsigned int numSegments = totalBytes / norm_segment_size; + norm_stream_bytes_remain = totalBytes % norm_segment_size; + norm_stream_buffer_count += numSegments; + } + else + { + numBytes = bytesAvailable; + norm_stream_buffer_count = norm_stream_buffer_max; + } + // 2) Write to the stream + unsigned int bytesWritten = NormStreamWrite(norm_tx_stream, buffer, numBytes); + //ASSERT(bytesWritten == numBytes); // this could happen if timer-based flow control is left enabled + // or if stream is closing ... ??? + // We had a "stream is closing" case here somehow? Need to make sure we don't try + // to write to norm stream + + // 3) Do we need to issue a watermark ACK request? + if (!norm_watermark_pending && (norm_stream_buffer_count >= (norm_stream_buffer_max >> 1))) + { + //TRACE("NorpSession::WriteToNormStream() initiating watermark ACK request (buffer count:%lu max:%lu usage:%u)...\n", + // norm_stream_buffer_count, norm_stream_buffer_max, NormStreamGetBufferUsage(norm_tx_stream)); + NormSetWatermark(norm_session, norm_tx_stream); + norm_watermark_pending = true; + } + return bytesWritten; + } + else + { + PLOG(PL_DETAIL, "NorpSession::WriteToNormStream() is blocked pending acknowledgment from receiver\n"); + return 0; + } +} // end NorpSession::WriteToNormStream() + +void NorpSession::FlushNormStream(bool eom, NormFlushMode flushMode) +{ + // NormStreamFlush always will transmit pending runt segments, if applicable + // (thus we need to manage our buffer counting accordingly if pending bytes remain) + NormStreamFlush(norm_tx_stream, eom, flushMode); + if (0 != norm_stream_bytes_remain) + { + norm_stream_buffer_count++; + norm_stream_bytes_remain = 0; + if (!norm_watermark_pending && (norm_stream_buffer_count >= (norm_stream_buffer_max >> 1))) + { + //TRACE("NorpSession::FlushNormStream() initiating watermark ACK request (buffer count:%lu max:%lu usage:%u)...\n", + // norm_stream_buffer_count, norm_stream_buffer_max); + NormSetWatermark(norm_session, norm_tx_stream, NormStreamGetBufferUsage(norm_tx_stream)); + norm_watermark_pending = true; + } + } +} // end NorpSession::FlushNormStream() + +bool NorpSession::OnNorpMsgTimeout(ProtoTimer& theTimer) +{ + PLOG(PL_DETAIL, "NorpSession::OnNorpMsgTimeout() repeat count = %d\n", theTimer.GetRepeatCount()); + if (0 != theTimer.GetRepeatCount()) + { + struct timeval currentTime; + ProtoSystemTime(currentTime); + norp_msg.SetTimestamp(currentTime); + if (SOCKS_PUT_REQUEST == socks_state) + { + if (!SendMessage(norp_msg)) + PLOG(PL_ERROR, "NorpSession::OnNorpMsgTimeout() warning: unable to send message (retries remaining %d)\n", theTimer.GetRepeatCount()); + } + else + { + if (!controller.SendMessage(norp_msg, norp_remote_addr)) + PLOG(PL_ERROR, "NorpSession::OnNorpMsgTimeout() warning: unable to send message (retries remaining %d)\n", theTimer.GetRepeatCount()); + } + } + else + { + PLOG(PL_ERROR, "NorpSession::OnNorpMsgTimeout() error: message retransmission timeout with no reply\n"); + switch (socks_state) + { + case SOCKS_PUT_REQUEST: + if (!MakeDirectConnect()) + { + PLOG(PL_ERROR, "NorpSession::OnNorpMsgTimeout() error: also unable to make direct TCP connection\n"); + Shutdown(); + } + return false; // MakeDirectConnect() kills timer + break; + case SOCKS_PUT_REPLY: + Shutdown(); + break; + case SOCKS_SHUTDOWN: + Close(); + return false; + default: + ASSERT(0); // shouldn't be messaging in any other states + break; + } + } + return true; +} // end NorpSession::OnNorpMsgTimeout() + +void NorpSession::OnNormEvent(NormEvent& theEvent) +{ + PLOG(PL_DETAIL, "NorpSession::OnNormEvent("); + switch (theEvent.type) + { + case NORM_TX_QUEUE_VACANCY: + PLOG(PL_DETAIL, "NORM_TX_QUEUE_VACANCY)\n"); + case NORM_TX_QUEUE_EMPTY: + if (NORM_TX_QUEUE_EMPTY == theEvent.type) PLOG(PL_DETAIL, "NORM_TX_QUEUE_EMPTY)\n"); + if (IsRemoteOriginator()) + { + // If there's pending data from the SOCKS client, try to send it + if (0 != client_pending) + { + if (!PutRemoteData()) + { + PLOG(PL_ERROR, "NorpSession::OnNormEvent(TX_VACANCY) error: PutRemoteData() failure!\n"); + Shutdown(); + } + } + } + else + { + if (0 != remote_pending) + { + if (!PutClientData()) + { + PLOG(PL_ERROR, "NorpSession::OnNormEvent(TX_VACANCY) error: PutClientData() failure!\n"); + Shutdown(); + } + } + } + break; + case NORM_TX_FLUSH_COMPLETED: + PLOG(PL_DETAIL, "NORM_TX_FLUSH_COMPLETED)\n"); + break; + case NORM_TX_WATERMARK_COMPLETED: + PLOG(PL_DETAIL, "NORM_TX_WATERMARK_COMPLETED)\n"); + if (NORM_ACK_SUCCESS == NormGetAckingStatus(norm_session)) + { + PLOG(PL_DETAIL, " acking status: SUCCESS\n"); + //TRACE("successful watermark ACK (buffer count:%lu max:%lu)\n", norm_stream_buffer_count, norm_stream_buffer_max); + norm_watermark_pending = false; + norm_stream_buffer_count -= (norm_stream_buffer_max >> 1); + //TRACE(" (count reduced to %lu)\n", norm_stream_buffer_count); + // Use as prompt to send pending data to NORM stream + if (IsRemoteOriginator()) + { + // If there's pending data from the SOCKS client, try to send it + if (0 != client_pending) + { + if (!PutRemoteData()) + { + PLOG(PL_ERROR, "NorpSession::OnNormEvent(TX_VACANCY) error: PutRemoteData() failure!\n"); + Shutdown(); + } + } + } + else + { + if (0 != remote_pending) + { + if (!PutClientData()) + { + PLOG(PL_ERROR, "NorpSession::OnNormEvent(TX_VACANCY) error: PutClientData() failure!\n"); + Shutdown(); + } + } + } + if (!persist_start_time.IsZero()) + persist_start_time.Zeroize(); // resets persistence timeout + } + else if (NORM_ACK_FAILURE == NormGetAckingStatus(norm_session)) + { + PLOG(PL_DETAIL, " acking status: FAILURE\n"); + if (persist_interval >= 0.0) + { + // Finite persistence will be observed + if (persist_start_time.IsZero()) + { + // Initialize persistence timeout + persist_start_time.GetCurrentTime(); + } + else + { + ProtoTime currentTime; + currentTime.GetCurrentTime(); + if ((currentTime - persist_start_time) > persist_interval) + { + NormStreamClose(norm_tx_stream); + norm_tx_stream = NORM_OBJECT_INVALID; + Shutdown(); + break; + } + } + } + NormResetWatermark(norm_session); + } + break; + case NORM_TX_CMD_SENT: + PLOG(PL_DETAIL, "NORM_TX_CMD_SENT)\n"); + break; + case NORM_TX_OBJECT_SENT: + PLOG(PL_DETAIL, "NORM_TX_OBJECT_SENT)\n"); + break; + case NORM_TX_OBJECT_PURGED: + PLOG(PL_DETAIL, "NORM_TX_OBJECT_PURGED)\n"); + if (theEvent.object == norm_tx_stream) + { + ASSERT(SOCKS_SHUTDOWN == socks_state); + // do we need to call NormStreamClose() here? + NormStreamClose(norm_tx_stream); + norm_tx_stream = NORM_OBJECT_INVALID; + if (ShutdownComplete()) Shutdown(); + } + break; + case NORM_TX_RATE_CHANGED: + PLOG(PL_DETAIL, "NORM_TX_RATE_CHANGED)\n"); + break; + case NORM_LOCAL_SENDER_CLOSED: + PLOG(PL_DETAIL, "NORM_LOCAL_SENDER_CLOSED)\n"); + break; + case NORM_REMOTE_SENDER_NEW: + PLOG(PL_DETAIL, "NORM_REMOTE_SENDER_NEW)\n"); + break; + case NORM_REMOTE_SENDER_ACTIVE: + PLOG(PL_DETAIL, "NORM_REMOTE_SENDER_ACTIVE)\n"); + break; + case NORM_REMOTE_SENDER_INACTIVE: + PLOG(PL_DETAIL, "NORM_REMOTE_SENDER_INACTIVE)\n"); + break; + case NORM_REMOTE_SENDER_PURGED: + PLOG(PL_DETAIL, "NORM_REMOTE_SENDER_PURGED)\n"); + break; + case NORM_RX_CMD_NEW: + PLOG(PL_DETAIL, "NORM_RX_CMD_NEW)\n"); + break; + case NORM_RX_OBJECT_NEW: + PLOG(PL_DETAIL, "NORM_RX_OBJECT_NEW)\n"); + // TBD - confirm it's a stream, etc? + norm_rx_stream = theEvent.object; + break; + case NORM_RX_OBJECT_INFO: + PLOG(PL_DETAIL, "NORM_RX_OBJECT_INFO)\n"); + break; + case NORM_RX_OBJECT_UPDATED: + PLOG(PL_DETAIL, "NORM_RX_OBJECT_UPDATED)\n"); + ASSERT(theEvent.object == norm_rx_stream); + norm_rx_pending = true; // signals there is NORM rx data ready for reading + if (IsRemoteOriginator()) + { + // we're the remote session originator (read data from NORM and put to local SOCKS client) + if (0 == remote_pending) + { + // try to read more data from NORM into "remote_buffer" and put to SOCKS client + if (!GetRemoteData()) + { + PLOG(PL_ERROR, "NorpSession::OnNormEvent(RX_UPDATE) error: GetRemoteData() failure!\n"); + Shutdown(); // TBD - shut down more gracefully (informing remote) + } + } + // else there's still data in remote_buffer pending relay to SOCKS client + // TBD - should we go ahead and append more data to remote_buffer? + // (for now, PutClientData() will call GetRemoteData() for more when relay to client completes + } + else + { + // we're the remote correspondent (read data from NORM and put to remote) + if ((0 == client_pending) && ((SOCKS_CONNECTED == socks_state) || (SOCKS_SHUTDOWN == socks_state))) + { + // try to read more data from NORM into "client_buffer" and put to SOCKS destination + if (!GetClientData()) + { + PLOG(PL_ERROR, "NorpSession::OnNormEvent(RX_UPDATE) error: GetRemoteData() failure!\n"); + Shutdown(); // TBD - shut down more gracefully (informing remote) + } + } + // else there's still data in client_buffer pending relay to TCP destination + // TBD - should we go ahead and append more data to client_buffer? + // (for now, PutRemoteData() will call GetClientData() for more when relay to remote completes + } + break; + case NORM_RX_OBJECT_COMPLETED: + PLOG(PL_DETAIL, "NORM_RX_OBJECT_COMPLETED)\n"); + if (IsRemoteOriginator()) + { + if (SOCKS_CONNECTED == socks_state) + { + socks_state = SOCKS_SHUTDOWN; + // Make sure our socks_client_socket gets shut down + if (0 == remote_pending) + PutClientData(); // will shutdown our socks_client_socket + // else socks_client_socket output notification will call PutClientData() + + // TBD - if the remote obj is completed, then there's no use in pushing more data their way? + // Gracefully shut down our norm_tx_stream (requesting an ACK from the correspondent) + NormStreamClose(norm_tx_stream, true); // Note our "trick" here to do a graceful close, _then_ watermark + NormSetWatermark(norm_session, norm_tx_stream, true); // future NORM API will add "bool watermark" option to graceful close + } + else + { + ASSERT(!socks_client_socket.IsOpen()); + } + } + else + { + if (SOCKS_CONNECTED == socks_state) + { + socks_state = SOCKS_SHUTDOWN; + if (0 == client_pending) + PutRemoteData(); // will shutdown our socks_remote_socket + // else socks_remote_socket output notification will call PutClientData() + + // Gracefully shut down our norm_tx_stream (requesting an ACK from the originator) + NormStreamClose(norm_tx_stream, true); // Note our "trick" here to do a graceful close, _then_ watermark + NormSetWatermark(norm_session, norm_tx_stream, true); // future NORM API will add "bool watermark" option to graceful close + } + else + { + ASSERT(!socks_remote_socket.IsOpen()); + } + } + norm_rx_stream = NORM_OBJECT_INVALID; + norm_rx_pending = false; + if (ShutdownComplete()) Close(); + break; + case NORM_RX_OBJECT_ABORTED: + PLOG(PL_DETAIL, "NORM_RX_OBJECT_ABORTED)\n"); + break; + case NORM_GRTT_UPDATED: + PLOG(PL_DETAIL, "NORM_GRTT_UPDATED)\n"); + break; + case NORM_CC_ACTIVE: + PLOG(PL_DETAIL, "NORM_CC_ACTIVE)\n"); + break; + case NORM_CC_INACTIVE: + PLOG(PL_DETAIL, "NORM_CC_INACTIVE)\n"); + break; + case NORM_EVENT_INVALID: + PLOG(PL_DETAIL, "NORM_EVENT_INVALID)\n"); + break; + default: + PLOG(PL_DETAIL, "UNKNOWN event %d)\n", theEvent.type); + break; + } +} // end NorpSession::OnNormEvent() + +NorpPreset::NorpPreset(Norp& norpController, UINT16 tcpPort, const ProtoAddress& dstAddr, const ProtoAddress& norpAddr) + : controller(norpController), tcp_port(tcpPort), dst_addr(dstAddr), norp_addr(norpAddr), server_socket(ProtoSocket::TCP) +{ + server_socket.SetNotifier(&(controller.GetSocketNotifier())); + server_socket.SetListener(this, &NorpPreset::OnSocketEvent); + +} + +NorpPreset::~NorpPreset() +{ + if (server_socket.IsOpen()) server_socket.Close(); +} + +bool NorpPreset::Listen() +{ + if (!server_socket.Listen(tcp_port)) + { + PLOG(PL_ERROR, "NorpPreset::Listen() error: unable to establish listening TCP socket on port %hu\n", tcp_port); + return false; + } + return true; +} // end NorpPreset::Listen(); + +void NorpPreset::OnSocketEvent(ProtoSocket& theSocket, ProtoSocket::Event theEvent) +{ + // TBD - dispatch ACCEPT events up to controller to create a corresponding + // jump-started NorpSession + + PLOG(PL_DETAIL, "NorpPreset::OnSocketEvent("); + switch (theEvent) + { + case ProtoSocket::INVALID_EVENT: + PLOG(PL_DETAIL, "INVALID_EVENT) ...\n"); + break; + case ProtoSocket::CONNECT: + PLOG(PL_DETAIL, "CONNECT) ...\n"); + break; + case ProtoSocket::ACCEPT: + { + PLOG(PL_DETAIL, "ACCEPT) ...\n"); + if (!controller.AcceptPresetClientConnection(*this)) + PLOG(PL_ERROR, "Norp::OnSocketEvent(ACCEPT) error: unable to create new NorpSession\n"); + break; + } + case ProtoSocket::SEND: + PLOG(PL_DETAIL, "SEND) ...\n"); + break; + case ProtoSocket::RECV: + { + PLOG(PL_DETAIL, "RECV) ...\n"); + break; + } + case ProtoSocket::DISCONNECT: + PLOG(PL_DETAIL, "DISCONNECT) ...\n"); + break; + + case ProtoSocket::EXCEPTION: + PLOG(PL_DETAIL, "EXCEPTION) ...\n"); + break; + + case ProtoSocket::ERROR_: + PLOG(PL_DETAIL, "ERROR_) ...\n"); + break; + } + +} // end NorpPreset::OnSocketEvent() + +Norp::Norp(ProtoDispatcher& theDispatcher) + : dispatcher(theDispatcher), socks_server_socket(ProtoSocket::TCP), socks_port(DEFAULT_SOCKS_PORT), + next_session_id(1), session_count(0), norp_rx_socket(ProtoSocket::UDP), norp_local_port(DEFAULT_NORP_PORT), + norp_remote_port(DEFAULT_NORP_PORT), norp_rtt_init(NorpSession::NORP_RTT_DEFAULT), + norm_enable(true), norm_instance(NORM_INSTANCE_INVALID), norm_node_id(NORM_NODE_ANY), + norm_port(DEFAULT_NORM_PORT), norm_cc_mode(NORM_CC), norm_tx_rate(DEFAULT_TX_RATE), norm_tx_limit(-1.0), + norm_segment_size(1400), norm_block_size(64), norm_parity_count(0), norm_parity_auto(0), norm_trace(false) +// ,port_pool(9000) +{ + socks_server_socket.SetNotifier(&GetSocketNotifier()); + socks_server_socket.SetListener(this, &Norp::OnSocksServerEvent); + norp_rx_socket.SetNotifier(&GetSocketNotifier()); + norp_rx_socket.SetListener(this, &Norp::OnNorpSocketEvent); +} + +Norp::~Norp() +{ + StopServer(); +} + +bool Norp::StartServer(bool normEnable) +{ + if (!socks_server_socket.Listen(socks_port)) + { + PLOG(PL_ERROR, "Norp::StartServer() error: socks server socket listen(%d) error\n", socks_port); + return false; + } + norm_enable = normEnable; + if (normEnable) + { + norm_instance = NormCreateInstance(); + if (NORM_INSTANCE_INVALID == norm_instance) + { + PLOG(PL_ERROR, "Norp::StartServer() error: NormCreateInstance() failure!\n"); + StopServer(); + return false; + } + NormDescriptor normDescriptor = NormGetDescriptor(norm_instance); + if (!dispatcher.InstallGenericInput(normDescriptor, NormEventCallback, this)) + { + PLOG(PL_ERROR, "Norp::StartServer() error: unable to install NORM event notification!\n"); + StopServer(); + return false; + } + if (!norp_rx_socket.Open(norp_local_port)) + { + PLOG(PL_ERROR, "Norp::StartServer() error: unable to open NORP signaling socket!\n"); + StopServer(); + return false; + } + if (NORM_NODE_ANY == norm_node_id) + norm_node_id = proxy_addr.GetEndIdentifier(); // TBD - make sure proxy_addr.IsValid() ??? + } + else + { + norm_node_id = NORM_NODE_NONE; + } + PLOG(PL_INFO, "norp: created controller on NORP port %hu with local NORM node id: %08x\n", norp_local_port, norm_node_id); + return true; +} // end Norp::StartServer() + +void Norp::StopServer() +{ + norp_rx_socket.Close(); + socks_server_socket.Close(); + session_list.Destroy(); + preset_list.Destroy(); + if (NORM_INSTANCE_INVALID != norm_instance) + { + dispatcher.RemoveGenericInput(NormGetDescriptor(norm_instance)); + NormDestroyInstance(norm_instance); + norm_instance = NORM_INSTANCE_INVALID; + } +} // end Norp::StopServer() + +void Norp::OnSessionClose(NorpSession& theSession) +{ + RemoveSession(theSession); + PLOG(PL_INFO, "norp: deleting closed session %u\n", theSession.GetSessionId()); + delete &theSession; +} // end Norp::OnSessionClose(); + +bool Norp::AcceptPresetClientConnection(NorpPreset& preset) +{ + // Create a new NorpSession for this prest client connection + NorpSession* norpSession = new NorpSession(*this, next_session_id++); + if (NULL == norpSession) + { + PLOG(PL_ERROR, "Norp::AcceptPresetClientConnection() new NorpSession error: %s\n", GetErrorString()); + return false; + } + if (!norpSession->AcceptPresetClientConnection(preset, norm_enable)) + { + PLOG(PL_ERROR, "Norp::AcceptPresetClientConnection() error: unable to accept connection\n"); + delete norpSession; + return false; + } + norpSession->SetProxyAddress(proxy_addr); + AddSession(*norpSession); + PLOG(PL_INFO, "norp: session %hu accepted preset connection from %s\n", norpSession->GetSessionId(), norpSession->GetClientAddress().GetHostString()); + return true; +} // end Norp::AcceptPresetClientConnection() + +void Norp::OnSocksServerEvent(ProtoSocket& theSocket, + ProtoSocket::Event theEvent) +{ + PLOG(PL_DETAIL, "Norp::OnServerSocketEvent("); + switch (theEvent) + { + case ProtoSocket::INVALID_EVENT: + PLOG(PL_DETAIL, "INVALID_EVENT) ...\n"); + break; + case ProtoSocket::CONNECT: + PLOG(PL_DETAIL, "CONNECT) ...\n"); + break; + case ProtoSocket::ACCEPT: + { + PLOG(PL_DETAIL, "ACCEPT) ...\n"); + // Create a new NorpSession for this potential client + NorpSession* norpSession = new NorpSession(*this, next_session_id++); + if (NULL == norpSession) + { + PLOG(PL_ERROR, "Norp::OnSocksServerEvent() new NorpSession error: %s\n", GetErrorString()); + break; + } + if (!norpSession->AcceptClientConnection(socks_server_socket, norm_enable)) + // Accept the socket connection into this session's "socks_client_socket" + //if (!socks_server_socket.Accept(&norpSession->AccessSocksClientSocket())) + { + PLOG(PL_ERROR, "Norp::OnSocksServerEvent(ACCEPT) error accepting connection\n"); + delete norpSession; + break; + } + norpSession->SetProxyAddress(proxy_addr); + AddSession(*norpSession); + PLOG(PL_INFO, "norp: session %hu accepted connection from %s\n", norpSession->GetSessionId(), norpSession->GetClientAddress().GetHostString()); + break; + } + case ProtoSocket::SEND: + PLOG(PL_DETAIL, "SEND) ...\n"); + break; + case ProtoSocket::RECV: + { + PLOG(PL_DETAIL, "RECV) ...\n"); + break; + } + case ProtoSocket::DISCONNECT: + PLOG(PL_DETAIL, "DISCONNECT) ...\n"); + break; + + case ProtoSocket::EXCEPTION: + PLOG(PL_DETAIL, "EXCEPTION) ...\n"); + break; + + case ProtoSocket::ERROR_: + PLOG(PL_DETAIL, "ERROR_) ...\n"); + break; + } +} // end Norp::OnSocksServerEvent() + +void Norp::AddSession(NorpSession& session) +{ + session_list.Insert(session); + session_count++; + if (norm_tx_limit >= 0.0) + { + double lowerLimit = 0.9 * (norm_tx_limit / (double)session_count); + NorpSessionList::Iterator iterator(session_list); + NorpSession* nextSession; + while (NULL != (nextSession = iterator.GetNextItem())) + nextSession->SetNormRateBounds(lowerLimit, norm_tx_limit); + } +} // end Norp::AddSession() + +void Norp::RemoveSession(NorpSession& session) +{ + session_list.Remove(session); + session_count--; + if (norm_tx_limit >= 0.0) + { + double lowerLimit = 0.9 * (norm_tx_limit / (double)session_count); + NorpSessionList::Iterator iterator(session_list); + NorpSession* nextSession; + while (NULL != (nextSession = iterator.GetNextItem())) + nextSession->SetNormRateBounds(lowerLimit, norm_tx_limit); + } +} // end Norp::RemoveSession() + + +bool Norp::AddPreset(UINT16 tcpPort, const ProtoAddress& dstAddr, const ProtoAddress& norpAddr) +{ + NorpPreset* preset = new NorpPreset(*this, tcpPort, dstAddr, norpAddr); + if (NULL == preset) + { + PLOG(PL_ERROR, "Norp::AddPreset() new NorpPreset error: %s\n", GetErrorString()); + return false; + } + if (!preset->Listen()) + { + PLOG(PL_ERROR, "Norp::AddPreset() error: unable to create listening TCP socket\n"); + delete preset; + return false; + } + preset_list.Insert(*preset); + return true; +} // end Norp::AddPreset() + +void Norp::OnNorpSocketEvent(ProtoSocket& theSocket, + ProtoSocket::Event theEvent) +{ + if (ProtoSocket::RECV == theEvent) + { + UINT32 buffer[1024/4]; + ProtoAddress senderAddr; + for(;;) + { + unsigned int numBytes = 1024; + if (!theSocket.RecvFrom((char*)buffer, numBytes, senderAddr)) + { + NorpSession* theSession = (NorpSession*)(theSocket.GetUserData()); + if ((NULL != theSession) && (NorpSession::SOCKS_PUT_REQUEST == theSession->GetSocksState())) + { + // Assume it is an ICMP "port unreachable" error in response to NORP request + PLOG(PL_INFO, "Norp::OnNorpSocketEvent() remote NORP server port unreachable?\n"); + if (!theSession->MakeDirectConnect()) + { + PLOG(PL_ERROR, "Norp::OnNorpSocketEvent(RECV) error: unable to make direct connection\n"); + theSession->Shutdown(); + } + break; + } + } + if (0 == numBytes) break; // nothing left to read + // TBD - make sure received command is at least 20 bytes (NORP header) + NorpMsg msg(buffer, numBytes); + switch (msg.GetType()) + { + case NorpMsg::SOCKS_REQ: + { + PLOG(PL_INFO, "Norp::OnNorpSocketEvent(RECV) received SOCKS_REQ session %d:%hu\n", msg.GetNodeId(), msg.GetSessionId()); + OnRemoteRequest(msg, senderAddr); + break; + } + case NorpMsg::REQ_ACK: + { + PLOG(PL_INFO, "Norp::OnNorpSocketEvent(RECV) received REQ_ACK session %d:%hu\n", msg.GetNodeId(), msg.GetSessionId()); + NorpSession* session = session_list.FindSession(msg.GetSessionId()); + // TBD - confirm the session found is for the given node id, etc + if (NULL != session) + { + if (!session->OnRemoteRequestAcknowledgment(msg, senderAddr)) + session->Shutdown(); + } + else + { + PLOG(PL_ERROR, "Norp::OnNorpSocketEvent(RECV) warning: received remote REQ_ACK for unknown session\n"); + } + break; + } + case NorpMsg::SOCKS_REP: + { + PLOG(PL_INFO, "Norp::OnNorpSocketEvent(RECV) received SOCKS_REP session %d:%hu (length:%d)\n", msg.GetNodeId(), msg.GetSessionId(), msg.GetLength()); + NorpSession* session = session_list.FindSession(msg.GetSessionId()); + if (NULL != session) + { + // TBD - confirm the session found is for the given node id, etc + if (!session->OnRemoteReply(msg, senderAddr)) + session->Shutdown(); + } + else + { + PLOG(PL_ERROR, "Norp::OnNorpSocketEvent(RECV) warning: received remote SOCKS_REPLY for unknown session\n"); + } + break; + } + case NorpMsg::REP_ACK: + { + PLOG(PL_INFO, "Norp::OnNorpSocketEvent(RECV) received REP_ACK session %d:%hu\n", msg.GetNodeId(), msg.GetSessionId()); + NorpSession* session = session_list.FindSession(msg.GetSessionId(), msg.GetNodeId()); + if (NULL != session) + { + if (!session->OnRemoteReplyAcknowledgment(msg)) + session->Shutdown(); + } + else + { + PLOG(PL_ERROR, "Norp::OnNorpSocketEvent(RECV) warning: received remote REP_ACK for unknown session\n"); + } + break; + } + case NorpMsg::ORIG_END: + { + + PLOG(PL_INFO, "Norp::OnNorpSocketEvent(RECV) received ORIG_END session %d:%hu\n", msg.GetNodeId(), msg.GetSessionId()); + NorpSession* session = session_list.FindSession(msg.GetSessionId(), msg.GetNodeId()); + if (NULL != session) + { + // TBD - mroe gently end things (e.g. let socket shutdowns commence)??? + session->Close(); // the session will make an upcall and get itself removed/deleted + } + UINT32 buffer[20/sizeof(UINT32)]; + NorpMsg ack(buffer, 20, false); + ack.SetType(NorpMsg::ACK_END); + ack.SetSessionId(msg.GetSessionId()); + ack.SetNodeId(NORM_NODE_NONE); + if (!SendMessage(ack, senderAddr)) + PLOG(PL_ERROR, "Norp::OnNorpSocketEvent(RECV) warning: unable to send ACK_END message to originator\n"); + break; + } + case NorpMsg::CORR_END: + { + PLOG(PL_INFO, "Norp::OnNorpSocketEvent(RECV) received CORR_END session %d:%hu\n", msg.GetNodeId(), msg.GetSessionId()); + NorpSession* session = session_list.FindSession(msg.GetSessionId(), NORM_NODE_NONE); + if (NULL != session) + { + // TBD - mroe gently end things (e.g. let socket shutdowns commence)??? + session->Close(); // the session will make an upcall and get itself removed/deleted + } + UINT32 buffer[20/sizeof(UINT32)]; + NorpMsg ack(buffer, 20, false); + ack.SetType(NorpMsg::ACK_END); + ack.SetSessionId(msg.GetSessionId()); + ack.SetNodeId(GetNormNodeId()); + if (!SendMessage(ack, senderAddr)) + PLOG(PL_ERROR, "Norp::OnNorpSocketEvent(RECV) warning: unable to send ACK_END message to correspondent\n"); + break; + } + case NorpMsg::ACK_END: + { + PLOG(PL_INFO, "Norp::OnNorpSocketEvent(RECV) received ACK_END session %d:%hu\n", msg.GetNodeId(), msg.GetSessionId()); + NorpSession* session = session_list.FindSession(msg.GetSessionId(), msg.GetNodeId()); + if (NULL != session) session->Close(); // the session will make an upcall and get itself removed/deleted + break; + } + default: + { + PLOG(PL_ERROR, "Norp::OnNormEvent(NORM_RX_CMD_NEW) error: received invalid NORP command: %d\n", msg.GetType()); + break; + } + } // end switch(msg.GetType()) + numBytes = 1024; + } // end while(theSocket.RecvFrom()) + } // end if (ProtoSocket::RECV == theEvent) + else + { + PLOG(PL_DETAIL, "unhandled NORP socket event!\n"); + } +} // end Norp::OnNorpSocketEvent() + +void Norp::OnRemoteRequest(const NorpMsg& msg, const ProtoAddress& senderAddr) +{ + NorpSession* session = session_list.FindSession(msg.GetSessionId(), msg.GetNodeId()); + if (NULL == session) + { + PLOG(PL_INFO, "norp: new remote session id %hu:%08x\n", msg.GetSessionId(), msg.GetNodeId()); + // It's a new remote session + session = new NorpSession(*this, msg.GetSessionId(), msg.GetNodeId()); + if (NULL == session) + { + PLOG(PL_ERROR, "Norp::OnRemoteRequest() new NorpSession error: %s\n", GetErrorString()); + // TBD - should we somehow signal the remote of our failure? + // (just let it time out for now) + return; + } + AddSession(*session); + } + if (!session->OnRemoteRequest(msg, senderAddr)) + { + PLOG(PL_ERROR, "Norp::OnRemoteRequest() error: unable to initiate new remote NorpSession\n"); + // TBD - signal failure to remote + RemoveSession(*session); + delete session; + } +} // end Norp::OnRemoteRequest() + + +void Norp::NormEventCallback(ProtoDispatcher::Descriptor descriptor, + ProtoDispatcher::Event theEvent, + const void* userData) +{ + if (ProtoDispatcher::EVENT_INPUT == theEvent) + { + Norp* norp = (Norp*)(userData); + norp->OnNormEvent(); + } + else + { + ASSERT(0); // should never happen + } +} // end Norp::NormEventCallback() + +void Norp::OnNormEvent() +{ + NormEvent theEvent; + // We make a looping, non-blocking call to NormGetNextEvent() here + // to consume all available NORM notification events + while (NormGetNextEvent(norm_instance, &theEvent, false)) + { + PLOG(PL_DETAIL, "Norp::OnNormEvent("); + // See if this event is for a "child" NorpSession + if (NORM_SESSION_INVALID != theEvent.session) + { + NorpSession* norpSession = (NorpSession*)NormGetUserData(theEvent.session); + if (NULL != norpSession) + { + PLOG(PL_DETAIL, "session %hu event)\n", norpSession->GetSessionId()); + norpSession->OnNormEvent(theEvent); + continue; + } + } + else + { + //ASSERT(0); // should never happen + } + } +} // end Norp::OnNormEvent() + + + diff --git a/norp/src/common/norpApp.cpp b/norp/src/common/norpApp.cpp new file mode 100644 index 0000000..df6cb89 --- /dev/null +++ b/norp/src/common/norpApp.cpp @@ -0,0 +1,378 @@ +#include "protoApp.h" +#include "protoNet.h" +#include "norp.h" +#include "norpVersion.h" + +#include // for stderr +#include // for atoi() + +class NorpApp : public ProtoApp +{ + public: + NorpApp(); + ~NorpApp(); + + // Required verrides from ProtoApp base + bool OnStartup(int argc, const char*const* argv); + bool ProcessCommands(int argc, const char*const* argv); + void OnShutdown(); + + private: + enum CmdType {CMD_INVALID, CMD_ARG, CMD_NOARG}; + static CmdType GetCmdType(const char* string); + bool OnCommand(const char* cmd, const char* val); + void Usage(); + + static const char* const CMD_LIST[]; + + Norp norp; + bool norm_enable; + +}; // end class NorpApp + +// Our application instance +PROTO_INSTANTIATE_APP(NorpApp) + + +NorpApp::NorpApp() + : norp(dispatcher), norm_enable(true) +{ +} + +NorpApp::~NorpApp() +{ + norp.StopServer(); +} + +void NorpApp::Usage() +{ + fprintf(stderr, "Usage: norp [interface ][address ][sport ][port ]\n" + " [norm {on|off}][id ][nport ][cce | ccl | rate ][persist ][limit ]\n" + " [correspondent ][forward ,/[,]][version]\n" + " [debug ][trace][dlog ][lport ][rport ]\n"); +} + +const char* const NorpApp::CMD_LIST[] = +{ + "+interface", // "public facing" interface (its address is used for bindings) + "+address", // explicit "public facing" address to use for bindings + "+sport", // SOCKS server port (7000 by default) + "+port", // NORP UDP signaling port number (7001 by default) + "+norm", // "on" or "off" to enable/disable NORM proxying ("on" by default) + "+id", // set local NormNodeId (auto set from IP address by default) + "+nport", // NORM port (UDP) (7002 by default) + "-cce", // Use NORM-CCE instead of NORM-CC + "-ccl", // Use NORM-CCL instead of NORM-CC + "+rate", // set fixed transmit rate (no congestion control) + "+limit", // set _cumulaltive_ NORP transmit rate limit + "+persist", // how long to persist NORM data delivery to receiver after TCP socket closure + "+debug", // set debug level + "-trace", // enables NORM protocol packet send/recv trace in debug output + "+dlog", // specify a file for debug logging + "+lport", // "local" NORP port (for loopback debugging) + "+rport", // "remote" NORP port (for loopback debugging) + "+forward", // ,/[,] to set up a preset proxy TCP listener + "+correspondent", // to route proxied connections through remote NORP proxy at given addr + "-version", // print version number and exit + NULL +}; + +NorpApp::CmdType NorpApp::GetCmdType(const char* cmd) +{ + if (!cmd) return CMD_INVALID; + unsigned int len = strlen(cmd); + bool matched = false; + CmdType type = CMD_INVALID; + const char* const* nextCmd = CMD_LIST; + while (*nextCmd) + { + if (!strncmp(cmd, *nextCmd+1, len)) + { + if (matched) + { + // ambiguous command (command should match only once) + return CMD_INVALID; + } + else + { + matched = true; + if ('+' == *nextCmd[0]) + type = CMD_ARG; + else + type = CMD_NOARG; + } + } + nextCmd++; + } + return type; +} // end NorpApp::GetCmdType() + +bool NorpApp::OnStartup(int argc, const char*const* argv) +{ + if (!ProcessCommands(argc, argv)) + { + PLOG(PL_ERROR, "NorpApp::OnStartup() error: bad command line\n"); + Usage(); + return false; + } + // If a proxy address wasn't set, use "default" local address for host + if (!norp.GetProxyAddress().IsValid()) + { + ProtoAddress theAddr; + if (theAddr.ResolveLocalAddress()) + norp.SetProxyAddress(theAddr); + else + PLOG(PL_WARN, "NorpApp::OnStartup() warning: proxy address undetermined!\n"); + } + PLOG(PL_INFO, "norp: starting proxy with address %s\n", norp.GetProxyAddress().GetHostString()); + + if (!norp.StartServer(norm_enable)) + { + PLOG(PL_ERROR, "NorpApp::OnStartup() error: norp server startup failure!\n"); + return false; + } + return true; +} // end NorpApp::OnStartup() + +void NorpApp::OnShutdown() +{ + norp.StopServer(); + CloseDebugLog(); +} // end Norp::OnShutdown() + +bool NorpApp::ProcessCommands(int argc, const char*const* argv) +{ + // Dispatch command-line commands to our OnCommand() method + int i = 1; + while ( i < argc) + { + // Is it a class NorpApp command? + switch (GetCmdType(argv[i])) + { + case CMD_INVALID: + { + PLOG(PL_ERROR, "NorpApp::ProcessCommands() Invalid command:%s\n", argv[i]); + return false; + } + case CMD_NOARG: + if (!OnCommand(argv[i], NULL)) + { + PLOG(PL_ERROR, "NorpApp::ProcessCommands() ProcessCommand(%s) error\n", argv[i]); + return false; + } + i++; + break; + case CMD_ARG: + if (!OnCommand(argv[i], argv[i+1])) + { + PLOG(PL_ERROR, "NorpApp::ProcessCommands() ProcessCommand(%s, %s) error\n", argv[i], argv[i+1]); + return false; + } + i += 2; + break; + } + } + return true; +} // end NorpApp::ProcessCommands() + +bool NorpApp::OnCommand(const char* cmd, const char* val) +{ + // (TBD) move command processing into Mgen class ??? + CmdType type = GetCmdType(cmd); + ASSERT(CMD_INVALID != type); + unsigned int len = strlen(cmd); + if ((CMD_ARG == type) && !val) + { + PLOG(PL_ERROR, "NorpApp::ProcessCommand(%s) missing argument\n", cmd); + return false; + } + else if (!strncmp("version", cmd, len)) + { + fprintf(stdout, "norp version %s\n", NORP_VERSION); + exit(0); + } + else if (!strncmp("debug", cmd, len)) + { + SetDebugLevel(atoi(val)); + } + else if (!strncmp("trace", cmd, len)) + { + norp.SetNormTrace(true); + } + else if (!strncmp("dlog", cmd, len)) + { + OpenDebugLog(val); + } + else if (!strncmp("interface", cmd, len)) + { + ProtoAddress ifaceAddr; + if (!ProtoNet::GetInterfaceAddress(val, ProtoAddress::IPv4, ifaceAddr)) + { + PLOG(PL_ERROR, "NorpApp::OnCommand(interface) error: unable to get address for interface \"%s\"\n", val); + return false; + } + norp.SetProxyAddress(ifaceAddr); + } + else if (!strncmp("address", cmd, len)) + { + ProtoAddress theAddr; + if (!theAddr.ResolveFromString(val)) + { + PLOG(PL_ERROR, "NorpApp::OnCommand(interface) error: invalid address \"%s\"\n", val); + return false; + } + norp.SetProxyAddress(theAddr); + } + else if (!strncmp("sport", cmd, len)) + { + norp.SetSocksPort(atoi(val)); + } + else if (!strncmp("port", cmd, len)) + { + UINT16 norpPort = atoi(val); + norp.SetLocalNorpPort(norpPort); + norp.SetRemoteNorpPort(norpPort); + } + else if (!strncmp("norm", cmd, len)) + { + if (0 == strcmp("on", val)) + norm_enable = true; + else + norm_enable = false; + } + else if (!strncmp("id", cmd, len)) + { + norp.SetNormNodeId(atoi(val)); + } + else if (!strncmp("nport", cmd, len)) + { + norp.SetNormPort(atoi(val)); + } + else if (!strncmp("cce", cmd, len)) + { + norp.SetNormCC(Norp::NORM_CCE); + } + else if (!strncmp("ccl", cmd, len)) + { + norp.SetNormCC(Norp::NORM_CCL); + } + else if (!strncmp("rate", cmd, len)) + { + double txRate; + if (1 != sscanf(val, "%lf", &txRate)) + { + PLOG(PL_ERROR, "NorpApp::OnCommand(rate) error: invalid rate \"%s\" bps\n", val); + return false; + } + norp.SetNormCC(Norp::NORM_FIXED); + norp.SetNormTxRate(txRate); + } + else if (!strncmp("limit", cmd, len)) + { + double txLimit; + if (1 != sscanf(val, "%lf", &txLimit)) + { + PLOG(PL_ERROR, "NorpApp::OnCommand(limit) error: invalid limit rate \"%s\" bps\n", val); + return false; + } + norp.SetNormTxLimit(txLimit); + } + else if (!strncmp("lport", cmd, len)) + { + norp.SetLocalNorpPort(atoi(val)); + } + else if (!strncmp("rport", cmd, len)) + { + norp.SetRemoteNorpPort(atoi(val)); + } + else if (!strncmp("forward", cmd, len)) + { + // ,/[,] + char* text = new char[strlen(val) + 1]; // copy so we can slice and dice + if (NULL == text) + { + PLOG(PL_ERROR, "NorpApp::OnCommand(%s) new char[] error: %s\n", cmd, GetErrorString()); + return false; + } + strcpy(text, val); + char* dstAddrPtr = strchr(text, ','); + if (NULL == dstAddrPtr) + { + PLOG(PL_ERROR, "NorpApp::OnCommand(%s, %s) error: missing !\n", cmd, val); + delete[] text; + return false; + } + *dstAddrPtr++ = '\0'; + char* dstPortPtr = strchr(dstAddrPtr, '/'); + if (NULL == dstPortPtr) + { + PLOG(PL_ERROR, "NorpApp::OnCommand(%s, %s) error: missing !\n", cmd, val); + delete[] text; + return false; + } + UINT16 tcpPort; + int result = sscanf(text, "%hu", &tcpPort); + if ((1 != result) || (0 == tcpPort)) + { + PLOG(PL_ERROR, "NorpApp::OnCommand(%s, %s) error: invalid \"%s\"!\n", cmd, val, text); + delete[] text; + return false; + } + *dstPortPtr++ = '\0'; + char* norpAddrPtr = strchr(dstPortPtr, ','); + if (NULL != norpAddrPtr) *norpAddrPtr++ = '\0'; + ProtoAddress dstAddr; + if (!dstAddr.ResolveFromString(dstAddrPtr)) + { + PLOG(PL_ERROR, "NorpApp::OnCommand(%s, %s) error: invalid \"%s\"!\n", cmd, val, dstAddrPtr); + delete[] text; + return false; + } + UINT16 dstPort; + result = sscanf(dstPortPtr, "%hu", &dstPort); + if ((1 != result) || (0 == dstPort)) + { + PLOG(PL_ERROR, "NorpApp::OnCommand(%s, %s) error: invalid \"%s\"!\n", cmd, val, text); + delete[] text; + return false; + } + dstAddr.SetPort(dstPort); + ProtoAddress norpAddr; + if (NULL != norpAddrPtr) + { + if (!norpAddr.ResolveFromString(norpAddrPtr)) + { + PLOG(PL_ERROR, "NorpApp::OnCommand(%s, %s) error: invalid \"%s\"!\n", cmd, val, norpAddrPtr); + delete[] text; + return false; + } + } + if (!norp.AddPreset(tcpPort, dstAddr, norpAddr)) + { + PLOG(PL_ERROR, "NorpApp::OnCommand(%s, %s) error: unable to add preset!\n", cmd, val); + delete[] text; + return false; + } + delete[] text; + } + else if (!strncmp("correspondent", cmd, len)) + { + // + ProtoAddress norpAddr; + if (!norpAddr.ResolveFromString(val)) + { + PLOG(PL_ERROR, "NorpApp::OnCommand(%s, %s) error: invalid !\n", cmd, val); + return false; + } + norp.SetRemoteNorpAddress(norpAddr); + } + else + { + PLOG(PL_ERROR, "NorpApp::OnCommand(%s) error: unimplemented command!\n", cmd); + return false; + } + return true; +} // end NorpApp::OnCommand() + + + diff --git a/norp/src/common/protoPktSOCKS.cpp b/norp/src/common/protoPktSOCKS.cpp new file mode 100644 index 0000000..b9fee4c --- /dev/null +++ b/norp/src/common/protoPktSOCKS.cpp @@ -0,0 +1,388 @@ +#include "protoPktSOCKS.h" +#include "protoDebug.h" + +ProtoPktSOCKS::AuthRequest::AuthRequest(UINT32* bufferPtr, unsigned int numBytes, bool initFromBuffer, bool freeOnDestruct) + : ProtoPkt(bufferPtr, numBytes, freeOnDestruct) +{ + if (initFromBuffer) InitFromBuffer(numBytes); +} + +ProtoPktSOCKS::AuthRequest::~AuthRequest() +{ +} + +ProtoPktSOCKS::AuthReply::AuthReply(UINT32* bufferPtr, unsigned int numBytes, bool initFromBuffer, bool freeOnDestruct) + : ProtoPkt(bufferPtr, numBytes, freeOnDestruct) +{ + if (initFromBuffer) InitFromBuffer(numBytes); +} + +ProtoPktSOCKS::AuthReply::~AuthReply() +{ +} + +ProtoPktSOCKS::Request::Request(UINT32* bufferPtr, unsigned int numBytes, bool initFromBuffer, bool freeOnDestruct) + : ProtoPkt(bufferPtr, numBytes, freeOnDestruct) +{ + if (initFromBuffer) InitFromBuffer(numBytes); +} + +ProtoPktSOCKS::Request::~Request() +{ +} + +UINT8 ProtoPktSOCKS::Request::GetAddressLength() const +{ + switch (GetAddressType()) + { + case IPv4: + return 4; + case NAME: + // First byte of "name" is length value + return ((UINT8*)buffer_ptr)[OFFSET_ADDR]; + case IPv6: + return 16; + default: + return 0; + } +} // end ProtoPktSOCKS::Request::GetAddressLength() + +UINT16 ProtoPktSOCKS::Request::GetPort() const +{ + const char* portPtr = GetAddressPtr() + GetAddressLength(); + UINT16 temp16; + memcpy(&temp16, portPtr, 2); + return ntohs(temp16); +} // end ProtoPktSOCKS::Request::GetPort() + + +bool ProtoPktSOCKS::Request::GetAddress(ProtoAddress& theAddr) const +{ + switch (GetAddressType()) + { + case IPv4: + { + theAddr.SetRawHostAddress(ProtoAddress::IPv4, GetAddressPtr(), 4); + break; + } + case NAME: + { + char name[256]; + UINT8 addrLen = GetAddressLength(); + strncpy(name, GetAddressPtr(), addrLen); + name[addrLen] = '\0'; + if (!theAddr.ResolveFromString(name)) + { + PLOG(PL_ERROR, "ProtoPktSOCKS::Request::GetAddress() error: unable to resolve name \"%s\" to addr!\n", name); + return false; + } break; + } + case IPv6: + { + theAddr.SetRawHostAddress(ProtoAddress::IPv6, GetAddressPtr(), 16); + break; + } + default: + { + PLOG(PL_ERROR, "ProtoPktSOCKS::Request::GetAddress() error: invalid address type!\n"); + return false; + } + } + theAddr.SetPort(GetPort()); + return true; +} // end ProtoPktSOCKS::Request::GetAddress() + +bool ProtoPktSOCKS::Request::SetAddress(const ProtoAddress& theAddr) +{ + AddrType addrType; + UINT8 addrLength; + switch (theAddr.GetType()) + { + case ProtoAddress::IPv4: + addrType = IPv4; + addrLength = 4; + break; + case ProtoAddress::IPv6: + addrType = IPv6; + addrLength = 16; + break; + default: + PLOG(PL_ERROR, "ProtoPktSOCKS::Request::SetAddress() error: invalid address type!\n"); + return false; + } + SetAddressType(addrType); + memcpy(AccessAddressPtr(), theAddr.GetRawHostAddress(), addrLength); + pkt_length = OFFSET_ADDR + addrLength; + + SetPort(theAddr.GetPort()); + + return true; +} // end ProtoPktSOCKS::Request::SetAddress() + +bool ProtoPktSOCKS::Request::SetPort(UINT16 thePort) +{ + char* portPtr = AccessAddressPtr() + GetAddressLength(); + UINT16 temp16 = htons(thePort); + memcpy(portPtr, (char*)&temp16, 2); + pkt_length = OFFSET_ADDR + GetAddressLength() + 2; + return true; +} // end ProtoPktSOCKS::Request::SetPort() + + +bool ProtoPktSOCKS::Request::SetName(const char* theName) +{ + SetAddressType(NAME); + unsigned int nameLength = strlen(theName); + // TBD - validate there is sufficient buffer space + memcpy(AccessAddressPtr(), theName, nameLength); + pkt_length += nameLength; + return true; +} // end ProtoPktSOCKS::Request::SetAddress() + +ProtoPktSOCKS::Reply::Reply(UINT32* bufferPtr, unsigned int numBytes, bool initFromBuffer, bool freeOnDestruct) + : ProtoPkt(bufferPtr, numBytes, freeOnDestruct) +{ + if (initFromBuffer) InitFromBuffer(numBytes); +} + +ProtoPktSOCKS::Reply::~Reply() +{ +} + +UINT8 ProtoPktSOCKS::Reply::GetAddressLength() const +{ + switch (GetAddressType()) + { + case IPv4: + return 4; + case NAME: + // First byte of "name" is length value + return ((UINT8*)buffer_ptr)[OFFSET_ADDR]; + case IPv6: + return 16; + default: + return 0; + } +} // end ProtoPktSOCKS::Reply::GetAddressLength() + +UINT16 ProtoPktSOCKS::Reply::GetPort() const +{ + const char* portPtr = GetAddressPtr() + GetAddressLength(); + UINT16 temp16; + memcpy(&temp16, portPtr, 2); + return ntohs(temp16); +} // end ProtoPktSOCKS::Reply::GetPort() + +bool ProtoPktSOCKS::Reply::GetAddress(ProtoAddress& theAddr) const +{ + switch (GetAddressType()) + { + case IPv4: + { + theAddr.SetRawHostAddress(ProtoAddress::IPv4, GetAddressPtr(), 4); + break; + } + case NAME: + { + char name[256]; + UINT8 addrLen = GetAddressLength(); + strncpy(name, GetAddressPtr(), addrLen); + name[addrLen] = '\0'; + if (!theAddr.ResolveFromString(name)) + { + PLOG(PL_ERROR, "ProtoPktSOCKS::Reply::GetAddress() error: unable to resolve name \"%s\" to addr!\n", name); + return false; + } break; + } + case IPv6: + { + theAddr.SetRawHostAddress(ProtoAddress::IPv6, GetAddressPtr(), 16); + break; + } + default: + { + PLOG(PL_ERROR, "ProtoPktSOCKS::Reply::GetAddress() error: invalid address type!\n"); + return false; + } + } + theAddr.SetPort(GetPort()); + return true; +} // end ProtoPktSOCKS::Reply::GetAddress() + +void ProtoPktSOCKS::Reply::SetAddress(AddrType addrType, const char* addrPtr, UINT8 addrLen) +{ + switch (addrType) + { + case ProtoPktSOCKS::IPv4: + ((UINT8*)buffer_ptr)[OFFSET_ATYPE] = (UINT8)ProtoPktSOCKS::IPv4; + memcpy(((char*)buffer_ptr) + OFFSET_ADDR, addrPtr, 4); + pkt_length = OFFSET_ADDR + 4; + break; + case ProtoPktSOCKS::NAME: + ((UINT8*)buffer_ptr)[OFFSET_ATYPE] = (UINT8)ProtoPktSOCKS::NAME; + ((UINT8*)buffer_ptr)[OFFSET_ADDR] = (UINT8)addrLen; + memcpy(((char*)buffer_ptr) + OFFSET_ADDR + 1, addrPtr, addrLen); + pkt_length = OFFSET_ADDR + 1 + addrLen; + break; + case ProtoPktSOCKS::IPv6: + ((UINT8*)buffer_ptr)[OFFSET_ATYPE] = (UINT8)ProtoPktSOCKS::IPv6; + memcpy(((char*)buffer_ptr) + OFFSET_ADDR, addrPtr, 16); + pkt_length = OFFSET_ADDR + 16; + break; + default: + PLOG(PL_ERROR, "ProtoPktSOCKS::Reply::SetAddress() invalid SOCKS address type\n"); + break; + } +} // end ProtoPktSOCKS::Reply::SetAddress() + +void ProtoPktSOCKS::Reply::SetPort(UINT16 thePort) +{ + UINT16 temp16 = htons(thePort); + char* portPtr = AccessAddressPtr() + GetAddressLength(); + memcpy(portPtr, &temp16, 2); + pkt_length = (GetAddressPtr() + GetAddressLength() + 2 - (char*)buffer_ptr); +} // end ProtoPktSOCKS::Reply::SetPort() + +void ProtoPktSOCKS::Reply::SetAddress(const ProtoAddress& theAddr) +{ + switch (theAddr.GetType()) + { + case ProtoAddress::IPv4: + SetAddress(ProtoPktSOCKS::IPv4, theAddr.GetRawHostAddress(), 4); + SetPort(theAddr.GetPort()); + break; + case ProtoAddress::IPv6: + SetAddress(ProtoPktSOCKS::IPv6, theAddr.GetRawHostAddress(), 16); + SetPort(theAddr.GetPort()); + break; + default: + PLOG(PL_ERROR, "ProtoPktSOCKS::Reply::SetAddress() invalid ProtoAddress type\n"); + break; + } +} // end ProtoPktSOCKS::Reply::SetAddress() + +ProtoPktSOCKS::UdpRequest::UdpRequest(UINT32* bufferPtr, unsigned int numBytes, bool initFromBuffer, bool freeOnDestruct) + : ProtoPkt(bufferPtr, numBytes, freeOnDestruct) +{ + if (initFromBuffer) InitFromBuffer(numBytes); +} + +ProtoPktSOCKS::UdpRequest::~UdpRequest() +{ +} + +UINT8 ProtoPktSOCKS::UdpRequest::GetAddressLength() const +{ + switch (GetAddressType()) + { + case IPv4: + return 4; + case NAME: + // First byte of "name" is length value + return ((UINT8*)buffer_ptr)[OFFSET_ADDR]; + case IPv6: + return 16; + default: + return 0; + } +} // end ProtoPktSOCKS::UdpRequest::GetAddressLength() + +UINT16 ProtoPktSOCKS::UdpRequest::GetPort() const +{ + const char* portPtr = GetAddressPtr() + GetAddressLength(); + UINT16 temp16; + memcpy(&temp16, portPtr, 2); + return ntohs(temp16); +} // end ProtoPktSOCKS::UdpRequest::GetPort() + + +bool ProtoPktSOCKS::UdpRequest::GetAddress(ProtoAddress& theAddr) const +{ + switch (GetAddressType()) + { + case IPv4: + { + theAddr.SetRawHostAddress(ProtoAddress::IPv4, GetAddressPtr(), 4); + break; + } + case NAME: + { + char name[256]; + UINT8 addrLen = GetAddressLength(); + strncpy(name, GetAddressPtr(), addrLen); + name[addrLen] = '\0'; + if (!theAddr.ResolveFromString(name)) + { + PLOG(PL_ERROR, "ProtoPktSOCKS::UdpRequest::GetAddress() error: unable to resolve name \"%s\" to addr!\n", name); + return false; + } + break; + } + case IPv6: + { + theAddr.SetRawHostAddress(ProtoAddress::IPv6, GetAddressPtr(), 16); + break; + } + default: + { + PLOG(PL_ERROR, "ProtoPktSOCKS::UdpRequest::GetAddress() error: invalid address type!\n"); + return false; + } + } + theAddr.SetPort(GetPort()); + return true; +} // end ProtoPktSOCKS::UdpRequest::GetAddress() + + +void ProtoPktSOCKS::UdpRequest::SetAddress(AddrType addrType, const char* addrPtr, UINT8 addrLen) +{ + switch (addrType) + { + case ProtoPktSOCKS::IPv4: + ((UINT8*)buffer_ptr)[OFFSET_ATYPE] = (UINT8)ProtoPktSOCKS::IPv4; + memcpy(((char*)buffer_ptr) + OFFSET_ADDR, addrPtr, 4); + pkt_length = OFFSET_ADDR + 4; + break; + case ProtoPktSOCKS::NAME: + ((UINT8*)buffer_ptr)[OFFSET_ATYPE] = (UINT8)ProtoPktSOCKS::NAME; + ((UINT8*)buffer_ptr)[OFFSET_ADDR] = (UINT8)addrLen; + memcpy(((char*)buffer_ptr) + OFFSET_ADDR + 1, addrPtr, addrLen); + pkt_length = OFFSET_ADDR + 1 + addrLen; + break; + case ProtoPktSOCKS::IPv6: + ((UINT8*)buffer_ptr)[OFFSET_ATYPE] = (UINT8)ProtoPktSOCKS::IPv6; + memcpy(((char*)buffer_ptr) + OFFSET_ADDR, addrPtr, 16); + pkt_length = OFFSET_ADDR + 16; + break; + default: + PLOG(PL_ERROR, "ProtoPktSOCKS::UdpRequest::SetAddress() invalid SOCKS address type\n"); + break; + } +} // end ProtoPktSOCKS::UdpRequest::SetAddress() + +void ProtoPktSOCKS::UdpRequest::SetPort(UINT16 thePort) +{ + UINT16 temp16 = htons(thePort); + char* portPtr = AccessAddressPtr() + GetAddressLength(); + memcpy(portPtr, &temp16, 2); + pkt_length = (GetAddressPtr() + GetAddressLength() + 2 - (char*)buffer_ptr); +} // end ProtoPktSOCKS::UdpRequest::SetPort() + +void ProtoPktSOCKS::UdpRequest::SetAddress(const ProtoAddress& theAddr) +{ + switch (theAddr.GetType()) + { + case ProtoAddress::IPv4: + SetAddress(ProtoPktSOCKS::IPv4, theAddr.GetRawHostAddress(), 4); + SetPort(theAddr.GetPort()); + break; + case ProtoAddress::IPv6: + SetAddress(ProtoPktSOCKS::IPv6, theAddr.GetRawHostAddress(), 16); + SetPort(theAddr.GetPort()); + break; + default: + PLOG(PL_ERROR, "ProtoPktSOCKS::UdpRequest::SetAddress() invalid ProtoAddress type\n"); + break; + } +} // end ProtoPktSOCKS::UdpRequest::SetAddress() +