├── .gitignore ├── COPYING ├── README ├── README.authenticator ├── README.host_checker ├── juniper-vpn.py ├── requirements.txt ├── sample.cfg └── tncc.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | 504 | 505 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Connecting to a Juniper VPN requires the generation of a DSID token. 2 | Generating this token involves authentication and host checking. The 3 | juniper-vpn.py script performs authentication, and the tncc.py script 4 | performs host checking. The DSID token can then be passed to openconnect. 5 | 6 | Alternatively, openconnect can perform the authentication steps on it's own 7 | and utilize the tncc.py script for host checker functionality. This is 8 | the recommended mode of operation. Instructions for using openconnect to 9 | perform authentication and tncc.py to perform host checking are contained in: 10 | 11 | README.host_checker 12 | 13 | Alternate instructions to allow juniper-vpn.py to perform authentication and 14 | tncc.py to perform host checking are contained in: 15 | 16 | README.authenticator 17 | 18 | Python dependencies can be found at `requirements.txt` file. 19 | -------------------------------------------------------------------------------- /README.authenticator: -------------------------------------------------------------------------------- 1 | Juniper VPN Authenticator: 2 | 3 | This script authenticates with a Juniper VPN server to generate a session 4 | cookie (DSID), and then passes that cookie to a VPN client (openconnect). 5 | 6 | Example usage with openconnect: 7 | 8 | ./juniper-vpn.py --host vpn.example.com --username joeuser \ 9 | --stdin DSID=%DSID% openconnect --juniper %HOST% --cookie-on-stdin 10 | 11 | This will connect to vpn.example.com and prompt the user for a authentication 12 | password. Once authenticated, the session cookie will be passed to openconnect 13 | which will connect to the VPN. Note that because the DSID provides full access 14 | to the VPN, it can be easily passed via stdin to avoid having it show up 15 | in the process list. 16 | 17 | Juniper Networks Host Checker: 18 | 19 | A python module, tncc.py, is integrated with juniper-vpn.py and provides 20 | support for VPN sites which require a host checker step. It is currently only 21 | tested on a subset of sites and does not yet support sites that require 22 | periodic host checker updates. 23 | 24 | Command line options: 25 | 26 | juniper-vpn.py [-h HOST] [-u USERNAME] [-o OATH] [-c CONFIG] [-s STDIN] \ 27 | 28 | 29 | -h --host 30 | VPN server to access. This option is required. 31 | 32 | -u --username 33 | Username to authenticate with. This option is required. 34 | 35 | -p --pass_prefix 36 | Optional, used for passwords composed of fixed prefix and variable 37 | postfix. This is fixed prefix part. 38 | 39 | -o --oath 40 | OATH key to use for OTP generation if required for authentication. 41 | Key should be in hex format. 42 | 43 | -c --config 44 | Config file. Rather than passing arguments on the command line, 45 | they can be contained within a config file. Command line arguments 46 | override config file options. See sample.cfg for documentation. 47 | 48 | -s --stdin 49 | Provide input to external program. This allows the cookie to be passed 50 | on stdin, avoiding having it appear in the process list. The string 51 | %DSID% will be replaced with the DSID cookie value. The string %HOST% 52 | will be replaced with the server hostname. 53 | 54 | -d --device-id 55 | The host checker can optionally pass a device ID. This may be required 56 | in some configurations. The device ID is a capitalized 32 character 57 | hex string and is typically found at the following register key in 58 | windows installations: 59 | HKEY_CURRENT_USER\Software\Juniper Networks\Device Id 60 | 61 | -f --enable-funk 62 | Some servers required host machine identification. This is refered to 63 | as a "funk message" in the documentation. The identification can 64 | include a system platform, a hostname, a list of network hardware 65 | addresses, and it may include a request for client certificates. 66 | Enabling this option causes the host checker to request a funk message. 67 | 68 | -H --hostname 69 | By default, the host checker sends the system's hostname in response to 70 | a funk message. This allows the hostname returned to be overridden 71 | 72 | -p --platform 73 | By default, the current system platform is returned in response to a 74 | funk message. This allows the platform identifier to be overridden with 75 | another identifier, such as 'Windows 7'. 76 | 77 | -a --hwaddr 78 | By default, the host checker reads the system's network mac addresses 79 | and sends them. This option allows that behavior be overridden by 80 | specifying a comma delimited list of hardware addresses to send instead. 81 | 82 | -C --certs 83 | The server may request client certificates in response to a funk 84 | message. This option accepts a comma delimited list of pem formatted 85 | certificates. The certificates are chosen from the list based on the 86 | requirements passed in the funk message. For use on Windows, the 87 | required certificate is generally the machine certificate. Information 88 | on accessing the machine certificate can be found here: 89 | https://wiki.strongswan.org/projects/strongswan/wiki/Win7Certs 90 | 91 | -U --user-agent 92 | The Juniper VPN server behaves differently based on the user agent 93 | string sent during the initial authentication step. This argument allows 94 | the user agent string to be overridden. 95 | 96 | 97 | Runs the external program with the supplied arguments when a cookie 98 | is generated. %DSID% in any argument is replaced with the DSID cookie 99 | value. %HOST% in any argument is replaced with the server hostname. 100 | If the external program returns a positive return code, it is assumed 101 | that a fatal error has occurred (such as bad command line arguments) 102 | and the script exits. If the external program returns -EPERM, it is 103 | assumed that the DSID is no longer valid and a new one is generated. 104 | For all other return codes, the external program is simply called again. 105 | An external program is required. 106 | 107 | 108 | Running without root or tun access: 109 | 110 | openconnect provides two options for running without any special permissions. 111 | The first option is to create a tun device in advance and configure permissions 112 | for user access. 113 | 114 | The second is to redirect the traffic for the tun device to an external program. 115 | This external program can then configure a user-level SOCKS proxy: 116 | 117 | ./juniper-vpn.py -c example.cfg -s DSID=%DSID% \ 118 | openconnect --juniper %HOST% --cookie-on-stdin --script-tun \ 119 | --script "tunsocks -D 1080" 120 | 121 | Both tunsocks and ocproxy can perform this role: 122 | 123 | http://github.com/russdill/tunsocks 124 | http://repo.or.cz/w/ocproxy.git 125 | 126 | -------------------------------------------------------------------------------- /README.host_checker: -------------------------------------------------------------------------------- 1 | Juniper VPN Host Checker: 2 | 3 | This script authenticates provides host checker functionality for use with 4 | openconnect. It operates as the CSD wrapper. 5 | 6 | Example usage with openconnect: 7 | 8 | TNCC_FUNK=1 TNCC_CERTS=cert1.pem openconnect --juniper --user joeuser \ 9 | --csd-wrapper tncc.py vpn.example.com 10 | 11 | openconnect will run the tncc.py script as part of the authentication process. 12 | 13 | Juniper Networks Host Checker: 14 | 15 | The host checker is currently only tested on a subset of sites and does not 16 | yet support sites that require periodic host checker updates. The behavior of 17 | the host checker can be modified by setting environmental variables. This is 18 | primarily required for funk support. 19 | 20 | TNCC_DEVICE_ID 21 | The host checker can optionally pass a device ID. This may be required 22 | in some configurations. The device ID is a capitalized 32 character 23 | hex string and is typically found at the following register key in 24 | windows installations: 25 | HKEY_CURRENT_USER\Software\Juniper Networks\Device Id 26 | 27 | TNCC_FUNK 28 | Some servers required host machine identification. This is refered to 29 | as a "funk message" in the documentation. The identification can 30 | include a system platform, a hostname, a list of network hardware 31 | addresses, and it may include a request for client certificates. 32 | Enabling this option causes the host checker to request a funk message. 33 | 34 | TNCC_HOSTNAME 35 | By default, the host checker sends the system's hostname in response to 36 | a funk message. This allows the hostname returned to be overridden 37 | 38 | TNCC_PLATFORM 39 | By default, the current system platform is returned in response to a 40 | funk message. This allows the platform identifier to be overridden with 41 | another identifier, such as 'Windows 7'. 42 | 43 | TNCC_HWADDR 44 | By default, the host checker reads the system's network mac addresses 45 | and sends them. This option allows that behavior be overridden by 46 | specifying a comma delimited list of hardware addresses to send instead. 47 | 48 | TNCC_CERTS 49 | The server may request client certificates in response to a funk 50 | message. This option accepts a comma delimited list of pem formatted 51 | certificates. The certificates are chosen from the list based on the 52 | requirements passed in the funk message. For use on Windows, the 53 | required certificate is generally the machine certificate. Information 54 | on accessing the machine certificate can be found here: 55 | https://wiki.strongswan.org/projects/strongswan/wiki/Win7Certs 56 | 57 | Running without root or tun access: 58 | 59 | openconnect provides two options for running without any special permissions. 60 | The first option is to create a tun device in advance and configure permissions 61 | for user access. 62 | 63 | The second is to redirect the traffic for the tun device to an external program. 64 | This external program can then configure a user-level SOCKS proxy: 65 | 66 | openconnect --juniper --user joeuser --csd-wrapper tncc.py \ 67 | --script-tun --script "tunsocks -D 1080" vpn.example.com 68 | 69 | Both tunsocks and ocproxy can perform this role: 70 | 71 | http://github.com/russdill/tunsocks 72 | http://repo.or.cz/w/ocproxy.git 73 | 74 | -------------------------------------------------------------------------------- /juniper-vpn.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import subprocess 5 | import mechanize 6 | import cookielib 7 | import getpass 8 | import sys 9 | import os 10 | import ssl 11 | import argparse 12 | import atexit 13 | import signal 14 | import ConfigParser 15 | import time 16 | import binascii 17 | import hmac 18 | import hashlib 19 | import shlex 20 | import tncc 21 | import platform 22 | import socket 23 | import netifaces 24 | import datetime 25 | 26 | debug = False 27 | 28 | ssl._create_default_https_context = ssl._create_unverified_context 29 | 30 | """ 31 | OATH code from https://github.com/bdauvergne/python-oath 32 | Copyright 2010, Benjamin Dauvergne 33 | 34 | * All rights reserved. 35 | * Redistribution and use in source and binary forms, with or without 36 | modification, are permitted provided that the following conditions are met: 37 | 38 | * Redistributions of source code must retain the above copyright 39 | notice, this list of conditions and the following disclaimer. 40 | * Redistributions in binary form must reproduce the above copyright 41 | notice, this list of conditions and the following disclaimer in the 42 | documentation and/or other materials provided with the distribution.''' 43 | """ 44 | 45 | def truncated_value(h): 46 | bytes = map(ord, h) 47 | offset = bytes[-1] & 0xf 48 | v = (bytes[offset] & 0x7f) << 24 | (bytes[offset+1] & 0xff) << 16 | \ 49 | (bytes[offset+2] & 0xff) << 8 | (bytes[offset+3] & 0xff) 50 | return v 51 | 52 | def dec(h,p): 53 | v = truncated_value(h) 54 | v = v % (10**p) 55 | return '%0*d' % (p, v) 56 | 57 | def int2beint64(i): 58 | hex_counter = hex(long(i))[2:-1] 59 | hex_counter = '0' * (16 - len(hex_counter)) + hex_counter 60 | bin_counter = binascii.unhexlify(hex_counter) 61 | return bin_counter 62 | 63 | def hotp(key): 64 | key = binascii.unhexlify(key) 65 | counter = int2beint64(int(time.time()) / 30) 66 | return dec(hmac.new(key, counter, hashlib.sha256).digest(), 6) 67 | 68 | class juniper_vpn(object): 69 | def __init__(self, args): 70 | self.args = args 71 | self.fixed_password = args.password is not None 72 | self.last_connect = 0 73 | 74 | if args.enable_funk: 75 | if not args.platform: 76 | args.platform = platform.system() + ' ' + platform.release() 77 | if not args.hostname: 78 | args.hostname = socket.gethostname() 79 | if not args.hwaddr: 80 | args.hwaddr = [] 81 | for iface in netifaces.interfaces(): 82 | try: 83 | mac = netifaces.ifaddresses(iface)[netifaces.AF_LINK][0]['addr'] 84 | assert mac != '00:00:00:00:00:00' 85 | args.hwaddr.append(mac) 86 | except: 87 | pass 88 | else: 89 | args.hwaddr = [n.strip() for n in args.hwaddr.split(',')] 90 | 91 | certs = [] 92 | if args.certs: 93 | now = datetime.datetime.now() 94 | for f in args.certs.split(','): 95 | cert = tncc.x509cert(f.strip()) 96 | if now < cert.not_before: 97 | print 'WARNING: %s is not yet valid' % f 98 | if now > cert.not_after: 99 | print 'WARNING: %s is expired' % f 100 | certs.append(cert) 101 | args.certs = [n.strip() for n in args.certs.split(',')] 102 | args.certs = certs 103 | 104 | self.br = mechanize.Browser() 105 | 106 | self.cj = cookielib.LWPCookieJar() 107 | self.br.set_cookiejar(self.cj) 108 | 109 | # Browser options 110 | self.br.set_handle_equiv(True) 111 | self.br.set_handle_redirect(True) 112 | self.br.set_handle_referer(True) 113 | self.br.set_handle_robots(False) 114 | 115 | # Follows refresh 0 but not hangs on refresh > 0 116 | self.br.set_handle_refresh(mechanize._http.HTTPRefreshProcessor(), 117 | max_time=1) 118 | 119 | # Want debugging messages? 120 | if debug: 121 | self.br.set_debug_http(True) 122 | self.br.set_debug_redirects(True) 123 | self.br.set_debug_responses(True) 124 | 125 | if args.user_agent: 126 | self.user_agent = args.user_agent 127 | else: 128 | self.user_agent = 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.1) Gecko/2008071615 Fedora/3.0.1-1.fc9 Firefox/3.0.1' 129 | 130 | self.br.addheaders = [('User-agent', self.user_agent)] 131 | 132 | self.last_action = None 133 | self.needs_2factor = False 134 | self.key = None 135 | self.pass_postfix = None 136 | 137 | def find_cookie(self, name): 138 | for cookie in self.cj: 139 | if cookie.name == name: 140 | return cookie 141 | return None 142 | 143 | def next_action(self): 144 | if self.find_cookie('DSID'): 145 | return 'connect' 146 | 147 | for form in self.br.forms(): 148 | if form.name == 'frmLogin': 149 | return 'login' 150 | elif form.name == 'frmDefender': 151 | return 'key' 152 | elif form.name == 'frmConfirmation': 153 | return 'continue' 154 | else: 155 | raise Exception('Unknown form type:', form.name) 156 | return 'tncc' 157 | 158 | def run(self): 159 | # Open landing page 160 | self.r = self.br.open('https://' + self.args.host) 161 | while True: 162 | action = self.next_action() 163 | if action == 'tncc': 164 | self.action_tncc() 165 | elif action == 'login': 166 | self.action_login() 167 | elif action == 'key': 168 | self.action_key() 169 | elif action == 'continue': 170 | self.action_continue() 171 | elif action == 'connect': 172 | self.action_connect() 173 | 174 | self.last_action = action 175 | 176 | def action_tncc(self): 177 | # Run tncc host checker 178 | dspreauth_cookie = self.find_cookie('DSPREAUTH') 179 | if dspreauth_cookie is None: 180 | raise Exception('Could not find DSPREAUTH key for host checker') 181 | 182 | dssignin_cookie = self.find_cookie('DSSIGNIN') 183 | t = tncc.tncc(self.args.host, args.device_id, args.enable_funk, 184 | args.platform, args.hostname, args.hwaddr, args.certs); 185 | self.cj.set_cookie(t.get_cookie(dspreauth_cookie, dssignin_cookie)) 186 | 187 | self.r = self.br.open(self.r.geturl()) 188 | 189 | def action_login(self): 190 | # The token used for two-factor is selected when this form is submitted. 191 | # If we aren't getting a password, then get the key now, otherwise 192 | # we could be sitting on the two factor key prompt later on waiting 193 | # on the user. 194 | 195 | # Enter username/password 196 | if self.args.username is None: 197 | self.args.username = raw_input('Username: ') 198 | if self.args.password is None or self.last_action == 'login': 199 | if self.fixed_password: 200 | print 'Login failed (Invalid username or password?)' 201 | sys.exit(1) 202 | else: 203 | self.args.password = getpass.getpass('Password: ') 204 | self.needs_2factor = False 205 | if self.args.pass_prefix: 206 | self.pass_postfix = getpass.getpass("Secondary password postfix:") 207 | if self.needs_2factor: 208 | if self.args.oath: 209 | self.key = hotp(self.args.oath) 210 | else: 211 | self.key = getpass.getpass('Two-factor key:') 212 | else: 213 | self.key = None 214 | 215 | self.br.select_form(nr=0) 216 | self.br.form['username'] = self.args.username 217 | self.br.form['password'] = self.args.password 218 | if self.args.pass_prefix: 219 | if self.pass_postfix: 220 | secondary_password = "".join([ self.args.pass_prefix, 221 | self.pass_postfix]) 222 | else: 223 | print 'Secondary password postfix not provided' 224 | sys.exit(1) 225 | self.br.form['password#2'] = secondary_password 226 | if self.args.realm: 227 | self.br.form['realm'] = [self.args.realm] 228 | self.r = self.br.submit() 229 | 230 | def action_key(self): 231 | # Enter key 232 | self.needs_2factor = True 233 | if self.args.oath: 234 | if self.last_action == 'key': 235 | print 'Login failed (Invalid OATH key)' 236 | sys.exit(1) 237 | self.key = hotp(self.args.oath) 238 | elif self.key is None: 239 | self.key = getpass.getpass('Two-factor key:') 240 | self.br.select_form(nr=0) 241 | self.br.form['password'] = self.key 242 | self.key = None 243 | self.r = self.br.submit() 244 | 245 | def action_continue(self): 246 | # Yes, I want to terminate the existing connection 247 | self.br.select_form(nr=0) 248 | self.r = self.br.submit() 249 | 250 | def action_connect(self): 251 | now = time.time() 252 | delay = 10.0 - (now - self.last_connect) 253 | if delay > 0: 254 | print 'Waiting %.0f...' % (delay) 255 | time.sleep(delay) 256 | self.last_connect = time.time(); 257 | 258 | dsid = self.find_cookie('DSID').value 259 | action = [] 260 | for arg in self.args.action: 261 | arg = arg.replace('%DSID%', dsid).replace('%HOST%', self.args.host) 262 | action.append(arg) 263 | 264 | p = subprocess.Popen(action, stdin=subprocess.PIPE) 265 | if args.stdin is not None: 266 | stdin = args.stdin.replace('%DSID%', dsid) 267 | stdin = stdin.replace('%HOST%', self.args.host) 268 | p.communicate(input = stdin) 269 | else: 270 | ret = p.wait() 271 | ret = p.returncode 272 | 273 | # Openconnect specific 274 | if ret == 2: 275 | self.cj.clear(self.args.host, '/', 'DSID') 276 | self.r = self.br.open(self.r.geturl()) 277 | 278 | def cleanup(): 279 | os.killpg(0, signal.SIGTERM) 280 | 281 | if __name__ == "__main__": 282 | 283 | parser = argparse.ArgumentParser(conflict_handler='resolve') 284 | parser.add_argument('-h', '--host', type=str, 285 | help='VPN host name') 286 | parser.add_argument('-r', '--realm', type=str, 287 | help='VPN realm') 288 | parser.add_argument('-u', '--username', type=str, 289 | help='User name') 290 | parser.add_argument('-p', '--pass_prefix', type=str, 291 | help="Secondary password prefix") 292 | parser.add_argument('-o', '--oath', type=str, 293 | help='OATH key for two factor authentication (hex)') 294 | parser.add_argument('-c', '--config', type=str, 295 | help='Config file') 296 | parser.add_argument('-s', '--stdin', type=str, 297 | help="String to pass to action's stdin") 298 | parser.add_argument('-d', '--device-id', type=str, 299 | help="Hex device ID") 300 | parser.add_argument('-f', '--enable-funk', action='store_true', 301 | help="Request funk message") 302 | parser.add_argument('-H', '--hostname', type=str, 303 | help="Hostname to pass with funk request") 304 | parser.add_argument('-p', '--platform', type=str, 305 | help="Platform type to pass with funk request") 306 | parser.add_argument('-a', '--hwaddr', type=str, 307 | help="Comma separated list of hwaddrs to pass with funk request") 308 | parser.add_argument('-C', '--certs', type=str, 309 | help="Comma separated list of pem formatted certificates for funk response") 310 | parser.add_argument('-U', '--user-agent', type=str, 311 | help="User agent string") 312 | parser.add_argument('action', nargs=argparse.REMAINDER, 313 | metavar=' []', 314 | help='External command') 315 | 316 | args = parser.parse_args() 317 | args.__dict__['username'] = None 318 | args.__dict__['password'] = None 319 | 320 | if len(args.action) and args.action[0] == '--': 321 | args.action = args.action[1:] 322 | 323 | if not len(args.action): 324 | args.action = None 325 | 326 | if args.config is not None: 327 | config = ConfigParser.RawConfigParser() 328 | config.read(args.config) 329 | for arg in ['username', 'host', 'password', 'oath', 'action', 'stdin', 330 | 'hostname', 'platform', 'hwaddr', 'certs', 'device_id', 331 | 'user_agent', 'pass_prefix', 'realm']: 332 | if args.__dict__[arg] is None: 333 | try: 334 | args.__dict__[arg] = config.get('vpn', arg) 335 | except: 336 | pass 337 | 338 | if not args.enable_funk: 339 | try: 340 | val = config.get('vpn', 'enable_funk').lower() 341 | if val in ['true', '1', 'yes', 'enable', 'on']: 342 | val = True 343 | elif val in ['false', '0', 'no', 'disable', 'off']: 344 | val = False 345 | else: 346 | raise Exception("Unable to parse funk argument", val) 347 | args.enable_funk = val 348 | except: 349 | pass 350 | 351 | if args.action is None: 352 | args.action = [] 353 | elif not isinstance(args.action, list): 354 | args.action = shlex.split(args.action) 355 | 356 | if args.host == None or args.action == []: 357 | print "--host and are required parameters" 358 | sys.exit(1) 359 | 360 | atexit.register(cleanup) 361 | jvpn = juniper_vpn(args) 362 | jvpn.run() 363 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # These versions work with `4e9c6ee` commit 2 | mechanize==0.3.6 3 | netifaces==0.10.6 4 | pycurl==7.43.0.1 5 | pyasn1-modules==0.2.1 6 | urlgrabber==3.10.2 7 | -------------------------------------------------------------------------------- /sample.cfg: -------------------------------------------------------------------------------- 1 | [vpn] 2 | 3 | username = joeUser 4 | host = juniper.vpn.host.somewhere 5 | password = nobodyknows 6 | oath = d41d8cd98f00b204e9800998ecf8427e 7 | 8 | stdin = DSID=%DSID% 9 | action = openconnect --juniper %HOST% --pass_prefix=1234 --cookie-on-stdin --script-tun 10 | --script "tunproxy -D 8080" 11 | 12 | user_agent = Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1 13 | 14 | device_id = D41D8CD98F00B204E9800998ECF8427E 15 | 16 | enable_funk = true 17 | platform = Windows 7 18 | hwaddr = 12:34:56:78:9A:BC, 21:43:65:87:A9:CB 19 | certs = machine_cert.pem 20 | hostname = joe-laptop 21 | -------------------------------------------------------------------------------- /tncc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import os 6 | import logging 7 | import StringIO 8 | import mechanize 9 | import cookielib 10 | import struct 11 | import socket 12 | import ssl 13 | import base64 14 | import collections 15 | import zlib 16 | import HTMLParser 17 | import socket 18 | import netifaces 19 | import urlgrabber 20 | import urllib2 21 | import platform 22 | import json 23 | import datetime 24 | import pyasn1_modules.pem 25 | import pyasn1_modules.rfc2459 26 | import pyasn1.codec.der.decoder 27 | import xml.etree.ElementTree 28 | 29 | ssl._create_default_https_context = ssl._create_unverified_context 30 | 31 | debug = False 32 | logging.basicConfig(stream=sys.stderr, level=logging.DEBUG if debug else logging.INFO) 33 | 34 | MSG_POLICY = 0x58316 35 | MSG_FUNK_PLATFORM = 0x58301 36 | MSG_FUNK = 0xa4c01 37 | 38 | 39 | # 0013 - Message 40 | def decode_0013(buf, indent): 41 | logging.debug('%scmd 0013 (Message) %d bytes', indent, len(buf)) 42 | ret = collections.defaultdict(list) 43 | while (len(buf) >= 12): 44 | length, cmd, out = decode_packet(buf, indent + " ") 45 | buf = buf[length:] 46 | ret[cmd].append(out) 47 | return ret 48 | 49 | # 0012 - u32 50 | def decode_0012(buf, indent): 51 | logging.debug('%scmd 0012 (u32) %d bytes', indent, len(buf)) 52 | return struct.unpack(">I", buf) 53 | 54 | # 0016 - zlib compressed message 55 | def decode_0016(buf, indent): 56 | logging.debug('%scmd 0016 (compressed message) %d bytes', indent, len(buf)) 57 | _, compressed = struct.unpack(">I" + str(len(buf) - 4) + "s", buf) 58 | buf = zlib.decompress(compressed) 59 | ret = collections.defaultdict(list) 60 | while (len(buf) >= 12): 61 | length, cmd, out = decode_packet(buf, indent + " ") 62 | buf = buf[length:] 63 | ret[cmd].append(out) 64 | return ret 65 | 66 | # 0ce4 - encapsulation 67 | def decode_0ce4(buf, indent): 68 | logging.debug('%scmd 0ce4 (encapsulation) %d bytes', indent, len(buf)) 69 | ret = collections.defaultdict(list) 70 | while (len(buf) >= 12): 71 | length, cmd, out = decode_packet(buf, indent + " ") 72 | buf = buf[length:] 73 | ret[cmd].append(out) 74 | return ret 75 | 76 | # 0ce5 - string without hex prefixer 77 | def decode_0ce5(buf, indent): 78 | s = struct.unpack(str(len(buf)) + "s", buf)[0] 79 | logging.debug('%scmd 0ce5 (string) %d bytes', indent, len(buf)) 80 | s = s.rstrip('\0') 81 | logging.debug('%s', s) 82 | return s 83 | 84 | # 0ce7 - string with hex prefixer 85 | def decode_0ce7(buf, indent): 86 | id, s = struct.unpack(">I" + str(len(buf) - 4) + "s", buf) 87 | logging.debug('%scmd 0ce7 (id %08x string) %d bytes', indent, id, len(buf)) 88 | 89 | if s.startswith('COMPRESSED:'): 90 | typ, length, data = s.split(':', 2) 91 | s = zlib.decompress(data) 92 | 93 | s = s.rstrip('\0') 94 | logging.debug('%s', s) 95 | return (id, s) 96 | 97 | # 0cf0 - encapsulation 98 | def decode_0cf0(buf, indent): 99 | logging.debug('%scmd 0cf0 (encapsulation) %d bytes', indent, len(buf)) 100 | ret = dict() 101 | cmd, _, out = decode_packet(buf, indent + " ") 102 | ret[cmd] = out 103 | return ret 104 | 105 | # 0cf1 - string without hex prefixer 106 | def decode_0cf1(buf, indent): 107 | s = struct.unpack(str(len(buf)) + "s", buf)[0] 108 | logging.debug('%scmd 0cf1 (string) %d bytes', indent, len(buf)) 109 | s = s.rstrip('\0') 110 | logging.debug('%s', s) 111 | return s 112 | 113 | # 0cf3 - u32 114 | def decode_0cf3(buf, indent): 115 | ret = struct.unpack(">I", buf) 116 | logging.debug('%scmd 0cf3 (u32) %d bytes - %d', indent, len(buf), ret[0]) 117 | return ret 118 | 119 | def decode_packet(buf, indent=""): 120 | cmd, _1, _2, length, _3 = struct.unpack(">IBBHI", buf[:12]) 121 | if length < 12: 122 | raise Exception("Invalid packet, cmd %04x, _1 %02x, _2 %02x, length %d" % (cmd, _1, _2, length)) 123 | 124 | data = buf[12:length] 125 | 126 | if length % 4: 127 | length += 4 - (length % 4) 128 | 129 | if cmd == 0x0013: 130 | data = decode_0013(data, indent) 131 | elif cmd == 0x0012: 132 | data = decode_0012(data, indent) 133 | elif cmd == 0x0016: 134 | data = decode_0016(data, indent) 135 | elif cmd == 0x0ce4: 136 | data = decode_0ce4(data, indent) 137 | elif cmd == 0x0ce5: 138 | data = decode_0ce5(data, indent) 139 | elif cmd == 0x0ce7: 140 | data = decode_0ce7(data, indent) 141 | elif cmd == 0x0cf0: 142 | data = decode_0cf0(data, indent) 143 | elif cmd == 0x0cf1: 144 | data = decode_0cf1(data, indent) 145 | elif cmd == 0x0cf3: 146 | data = decode_0cf3(data, indent) 147 | else: 148 | logging.debug('%scmd %04x(%02x:%02x) is unknown, length %d', indent, cmd, _1, _2, length) 149 | data = None 150 | 151 | return length, cmd, data 152 | 153 | def encode_packet(cmd, align, buf): 154 | align = 4 155 | orig_len = len(buf) 156 | if align > 1 and (len(buf) + 12) % align: 157 | buf += struct.pack(str(align - len(buf) % align) + "x") 158 | 159 | return struct.pack(">IBBHI", cmd, 0xc0, 0x00, orig_len + 12, 0x0000583) + buf 160 | 161 | # 0013 - Message 162 | def encode_0013(buf): 163 | return encode_packet(0x0013, 4, buf) 164 | 165 | # 0012 - u32 166 | def encode_0012(i): 167 | return encode_packet(0x0012, 1, struct.pack("I" + str(len(s)) + "sx", 181 | prefix, s)) 182 | 183 | # 0cf0 - encapsulation 184 | def encode_0cf0(buf): 185 | return encode_packet(0x0cf0, 4, buf) 186 | 187 | # 0cf1 - string without hex prefixer 188 | def encode_0cf1(s): 189 | s += '\0' 190 | return encode_packet(0x0ce5, 1, struct.pack(str(len(s)) + "s", s)) 191 | 192 | # 0cf3 - u32 193 | def encode_0cf3(i): 194 | return encode_packet(0x0013, 1, struct.pack(" 0 287 | self.br.set_handle_refresh(mechanize._http.HTTPRefreshProcessor(), 288 | max_time=1) 289 | 290 | # Want debugging messages? 291 | if debug: 292 | self.br.set_debug_http(True) 293 | self.br.set_debug_redirects(True) 294 | self.br.set_debug_responses(True) 295 | 296 | self.user_agent = 'Neoteris HC Http' 297 | self.br.addheaders = [('User-agent', self.user_agent)] 298 | 299 | def find_cookie(self, name): 300 | for cookie in self.cj: 301 | if cookie.name == name: 302 | return cookie 303 | return None 304 | 305 | def set_cookie(self, name, value): 306 | cookie = cookielib.Cookie(version=0, name=name, value=value, 307 | port=None, port_specified=False, domain=self.vpn_host, 308 | domain_specified=True, domain_initial_dot=False, path=self.path, 309 | path_specified=True, secure=True, expires=None, discard=True, 310 | comment=None, comment_url=None, rest=None, rfc2109=False) 311 | self.cj.set_cookie(cookie) 312 | 313 | def parse_response(self): 314 | # Read in key/token fields in HTTP response 315 | response = dict() 316 | last_key = '' 317 | for line in self.r.readlines(): 318 | line = line.strip() 319 | # Note that msg is too long and gets wrapped, handle it special 320 | if last_key == 'msg' and len(line): 321 | response['msg'] += line 322 | else: 323 | key = '' 324 | try: 325 | key, val = line.split('=', 1) 326 | response[key] = val 327 | except: 328 | pass 329 | last_key = key 330 | return response 331 | 332 | def parse_policy_response(self, msg_data): 333 | # The decompressed data is HTMLish, decode it. The value="" of each 334 | # tag is the data we want. 335 | objs = [] 336 | class ParamHTMLParser(HTMLParser.HTMLParser): 337 | def handle_starttag(self, tag, attrs): 338 | if tag.lower() == 'param': 339 | for key, value in attrs: 340 | if key.lower() == 'value': 341 | # It's made up of a bunch of key=value pairs separated 342 | # by semicolons 343 | d = dict() 344 | for field in value.split(';'): 345 | field = field.strip() 346 | try: 347 | key, value = field.split('=', 1) 348 | d[key] = value 349 | except: 350 | pass 351 | objs.append(d) 352 | p = ParamHTMLParser() 353 | p.feed(msg_data) 354 | p.close() 355 | return objs 356 | 357 | def parse_funk_response(self, msg_data): 358 | e = xml.etree.ElementTree.fromstring(msg_data) 359 | req_certs = dict() 360 | for cert in e.find('AttributeRequest').findall('CertData'): 361 | dns = dict() 362 | cert_id = cert.attrib['Id'] 363 | for attr in cert.findall('Attribute'): 364 | name = attr.attrib['Name'] 365 | value = attr.attrib['Value'] 366 | attr_type = attr.attrib['Type'] 367 | if attr_type == 'DN': 368 | dns[name] = dict(n.strip().split('=') for n in value.split(',')) 369 | else: 370 | # Unknown attribute type 371 | pass 372 | req_certs[cert_id] = dns 373 | return req_certs 374 | 375 | def gen_funk_platform(self): 376 | # We don't know if the xml parser on the other end is fully complaint, 377 | # just format a string like it expects. 378 | 379 | msg = " " % self.platform 380 | msg += " " 381 | 382 | def add_attr(key, val): 383 | return "" % (key, val) 384 | 385 | msg += add_attr('Platform', self.platform) 386 | if self.hostname: 387 | msg += add_attr(self.hostname, 'NETBIOSName') # Reversed 388 | 389 | for mac in self.mac_addrs: 390 | msg += add_attr(mac, 'MACAddress') # Reversed 391 | 392 | msg += " " 393 | 394 | return encode_0ce7(msg, MSG_FUNK_PLATFORM) 395 | 396 | def gen_funk_present(self): 397 | msg = " " % self.platform 398 | msg += " " 399 | return encode_0ce7(msg, MSG_FUNK) 400 | 401 | def gen_funk_response(self, certs): 402 | 403 | msg = " " % self.platform 404 | msg += " " 405 | msg += "" % self.platform 406 | for name, value in certs.iteritems(): 407 | msg += "" % (name, value.data.strip()) 408 | msg += "" % (name, value.data.strip()) 409 | msg += " " 410 | 411 | return encode_0ce7(msg, MSG_FUNK) 412 | 413 | def gen_policy_request(self): 414 | policy_blocks = collections.OrderedDict({ 415 | 'policy_request': { 416 | 'message_version': '3' 417 | }, 418 | 'esap': { 419 | 'esap_version': 'NOT_AVAILABLE', 420 | 'fileinfo': 'NOT_AVAILABLE', 421 | 'has_file_versions': 'YES', 422 | 'needs_exact_sdk': 'YES', 423 | 'opswat_sdk_version': '3' 424 | }, 425 | 'system_info': { 426 | 'os_version': '2.6.2', 427 | 'sp_version': '0', 428 | 'hc_mode': 'userMode' 429 | } 430 | }) 431 | 432 | msg = '' 433 | for policy_key, policy_val in policy_blocks.iteritems(): 434 | v = ''.join([ '%s=%s;' % (k, v) for k, v in policy_val.iteritems()]) 435 | msg += '' % (policy_key, v) 436 | 437 | return encode_0ce7(msg, 0xa4c18) 438 | 439 | def gen_policy_response(self, policy_objs): 440 | # Make a set of policies 441 | policies = set() 442 | for entry in policy_objs: 443 | if 'policy' in entry: 444 | policies.add(entry['policy']) 445 | 446 | # Try to determine on policy name whether the response should be OK 447 | # or NOTOK. Default to OK if we don't know, this may need updating. 448 | msg = '' 449 | for policy in policies: 450 | msg += '\npolicy:%s\nstatus:' % policy 451 | if 'Unsupported' in policy or 'Deny' in policy: 452 | msg += 'NOTOK\nerror:Unknown error' 453 | elif 'Required' in policy: 454 | msg += 'OK\n' 455 | else: 456 | # Default action 457 | msg += 'OK\n' 458 | 459 | return encode_0ce7(msg, MSG_POLICY) 460 | 461 | def get_cookie(self, dspreauth=None, dssignin=None): 462 | 463 | if dspreauth is None or dssignin is None: 464 | self.r = self.br.open('https://' + self.vpn_host) 465 | else: 466 | try: 467 | self.cj.set_cookie(dspreauth) 468 | except: 469 | self.set_cookie('DSPREAUTH', dspreauth) 470 | try: 471 | self.cj.set_cookie(dssignin) 472 | except: 473 | self.set_cookie('DSSIGNIN', dssignin) 474 | 475 | inner = self.gen_policy_request() 476 | inner += encode_0ce7('policy request\x00v4', MSG_POLICY) 477 | if self.funk: 478 | inner += self.gen_funk_platform() 479 | inner += self.gen_funk_present() 480 | 481 | msg_raw = encode_0013(encode_0ce4(inner) + encode_0ce5('Accept-Language: en') + encode_0cf3(1)) 482 | logging.debug('Sending packet -') 483 | decode_packet(msg_raw) 484 | 485 | post_attrs = { 486 | 'connID': '0', 487 | 'timestamp': '0', 488 | 'msg': base64.b64encode(msg_raw), 489 | 'firsttime': '1' 490 | } 491 | if self.deviceid: 492 | post_attrs['deviceid'] = self.deviceid 493 | 494 | post_data = ''.join([ '%s=%s;' % (k, v) for k, v in post_attrs.iteritems()]) 495 | self.r = self.br.open('https://' + self.vpn_host + self.path + 'hc/tnchcupdate.cgi', post_data) 496 | 497 | # Parse the data returned into a key/value dict 498 | response = self.parse_response() 499 | 500 | # msg has the stuff we want, it's base64 encoded 501 | logging.debug('Receiving packet -') 502 | msg_raw = base64.b64decode(response['msg']) 503 | _1, _2, msg_decoded = decode_packet(msg_raw) 504 | 505 | # Within msg, there is a field of data 506 | sub_strings = msg_decoded[0x0ce4][0][0x0ce7] 507 | 508 | # Pull the data out of the 'value' key in the htmlish stuff returned 509 | policy_objs = [] 510 | req_certs = dict() 511 | for str_id, sub_str in sub_strings: 512 | if str_id == MSG_POLICY: 513 | policy_objs += self.parse_policy_response(sub_str) 514 | elif str_id == MSG_FUNK: 515 | req_certs = self.parse_funk_response(sub_str) 516 | 517 | if debug: 518 | for obj in policy_objs: 519 | if 'policy' in obj: 520 | logging.debug('policy %s', obj['policy']) 521 | for key, val in obj.iteritems(): 522 | if key != 'policy': 523 | logging.debug('\t%s %s', key, val) 524 | 525 | # Try to locate the required certificates 526 | certs = dict() 527 | for cert_id, req_dns in req_certs.iteritems(): 528 | for cert in self.avail_certs: 529 | fail = False 530 | for dn_name, dn_vals in req_dns.iteritems(): 531 | for name, val in dn_vals.iteritems(): 532 | try: 533 | if dn_name == 'IssuerDN': 534 | assert val in cert.issuer[name] 535 | else: 536 | logging.warn('Unknown DN type %s', str(dn_name)) 537 | raise Exception() 538 | except: 539 | fail = True 540 | break 541 | if fail: 542 | break 543 | if not fail: 544 | certs[cert_id] = cert 545 | break 546 | if cert_id not in certs: 547 | logging.warn('Could not find certificate for %s', str(req_dns)) 548 | 549 | inner = '' 550 | if certs: 551 | inner += self.gen_funk_response(certs) 552 | inner += self.gen_policy_response(policy_objs) 553 | 554 | msg_raw = encode_0013(encode_0ce4(inner) + encode_0ce5('Accept-Language: en')) 555 | logging.debug('Sending packet -') 556 | decode_packet(msg_raw) 557 | 558 | post_attrs = { 559 | 'connID': '1', 560 | 'msg': base64.b64encode(msg_raw), 561 | 'firsttime': '1' 562 | } 563 | 564 | post_data = ''.join([ '%s=%s;' % (k, v) for k, v in post_attrs.iteritems()]) 565 | self.r = self.br.open('https://' + self.vpn_host + self.path + 'hc/tnchcupdate.cgi', post_data) 566 | 567 | # We have a new DSPREAUTH cookie 568 | return self.find_cookie('DSPREAUTH') 569 | 570 | class tncc_server(object): 571 | def __init__(self, s, t): 572 | self.sock = s 573 | self.tncc = t 574 | 575 | def process_cmd(self): 576 | buf = sock.recv(1024).decode('ascii') 577 | if not len(buf): 578 | sys.exit(0) 579 | cmd, buf = buf.split('\n', 1) 580 | cmd = cmd.strip() 581 | args = dict() 582 | for n in buf.split('\n'): 583 | n = n.strip() 584 | if len(n): 585 | key, val = n.strip().split('=', 1) 586 | args[key] = val 587 | if cmd == 'start': 588 | cookie = self.tncc.get_cookie(args['Cookie'], args['DSSIGNIN']) 589 | resp = '200\n3\n%s\n\n' % cookie.value 590 | sock.send(resp.encode('ascii')) 591 | elif cmd == 'setcookie': 592 | # FIXME: Support for periodic updates 593 | dsid_value = args['Cookie'] 594 | 595 | if __name__ == "__main__": 596 | vpn_host = sys.argv[1] 597 | 598 | funk = 'TNCC_FUNK' in os.environ and os.environ['TNCC_FUNK'] != '0' 599 | 600 | platform = os.environ.get('TNCC_PLATFORM', platform.system() + ' ' + platform.release()) 601 | 602 | if 'TNCC_HWADDR' in os.environ: 603 | mac_addrs = [n.strip() for n in os.environ['TNCC_HWADDR'].split(',')] 604 | else: 605 | mac_addrs = [] 606 | for iface in netifaces.interfaces(): 607 | try: 608 | mac = netifaces.ifaddresses(iface)[netifaces.AF_LINK][0]['addr'] 609 | assert mac != '00:00:00:00:00:00' 610 | mac_addrs.append(mac) 611 | except: 612 | pass 613 | 614 | hostname = os.environ.get('TNCC_HOSTNAME', socket.gethostname()) 615 | 616 | certs = [] 617 | if 'TNCC_CERTS' in os.environ: 618 | now = datetime.datetime.now() 619 | for f in os.environ['TNCC_CERTS'].split(','): 620 | cert = x509cert(f.strip()) 621 | if now < cert.not_before: 622 | logging.warn('WARNING: %s is not yet valid', f) 623 | if now > cert.not_after: 624 | logging.warn('WARNING: %s is expired', f) 625 | certs.append(cert) 626 | 627 | # \HKEY_CURRENT_USER\Software\Juniper Networks\Device Id 628 | device_id = os.environ.get('TNCC_DEVICE_ID') 629 | 630 | t = tncc(vpn_host, device_id, funk, platform, hostname, mac_addrs, certs) 631 | 632 | if len(sys.argv) == 4: 633 | dspreauth_value = sys.argv[2] 634 | dssignin_value = sys.argv[3] 635 | 'TNCC ', dspreauth_value, dssignin_value 636 | print t.get_cookie(dspreauth, dssignin).value 637 | else: 638 | sock = socket.fromfd(0, socket.AF_UNIX, socket.SOCK_SEQPACKET) 639 | server = tncc_server(sock, t) 640 | while True: 641 | server.process_cmd() 642 | 643 | --------------------------------------------------------------------------------