1515# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
1717import sys
18+ import re
1819import socket
1920import ipaddress
21+ import subprocess
2022
2123from vyos .utils .network import interface_list
2224from vyos .utils .network import vrf_list
156158 6 : '/bin/ping6' ,
157159}
158160
161+ tcp_options = {
162+ 'count' : {'type' : '<requests>' , 'help' : 'Number of requests to send' },
163+ 'interface' : {
164+ 'type' : '<interface>' ,
165+ 'helpfunction' : interface_list ,
166+ 'help' : 'Source interface' ,
167+ },
168+ 'port' : {'type' : '<port>' , 'help' : 'Destination port' },
169+ 'source-address' : {'type' : '<x.x.x.x> <h:h:h:h:h:h:h:h>' , 'help' : 'Source address' },
170+ 'vrf' : {
171+ 'type' : '<vrf>' ,
172+ 'help' : 'Use specified VRF table' ,
173+ 'helpfunction' : vrf_list ,
174+ 'dflt' : 'default' ,
175+ },
176+ }
177+
178+ ping_options = options | {
179+ 'tcp' : {'type' : 'noarg' , 'help' : 'Use TCP connect probe' },
180+ }
181+
182+ tcp_completion_options = {'tcp' : ping_options ['tcp' ]} | tcp_options
183+
184+ NPING = '/usr/bin/nping'
185+ TCP_DELAY = '1s'
186+ SUCCESSFUL_CONNECTIONS_RE = re .compile (r'Successful connections:\s+(\d+)' )
187+
159188
160189class List (list ):
161190 def first (self ):
@@ -179,7 +208,7 @@ def completion_failure(option: str) -> None:
179208 sys .exit (1 )
180209
181210
182- def expension_failure (option , completions ):
211+ def expansion_failure (option , completions ):
183212 reason = 'Ambiguous' if completions else 'Invalid'
184213 sys .stderr .write (
185214 '\n \n {} command: {} [{}]\n \n ' .format (reason , ' ' .join (sys .argv ),
@@ -192,16 +221,20 @@ def expension_failure(option, completions):
192221 sys .exit (1 )
193222
194223
195- def complete (prefix ):
196- return [o for o in options if o .startswith (prefix )]
224+ def complete (prefix , available_options = options ):
225+ return [o for o in available_options if o .startswith (prefix )]
226+
227+
228+ class UsageError (Exception ):
229+ pass
197230
198231
199232def convert (command , args ):
200233 while args :
201234 shortname = args .first ()
202235 longnames = complete (shortname )
203236 if len (longnames ) != 1 :
204- expension_failure (shortname , longnames )
237+ expansion_failure (shortname , longnames )
205238 longname = longnames [0 ]
206239 if options [longname ]['type' ] == 'noarg' :
207240 command = options [longname ]['ping' ].format (
@@ -214,6 +247,249 @@ def convert(command, args):
214247 return command
215248
216249
250+ def option_value_help (option , available_options ):
251+ helplines = available_options [option ]['type' ]
252+ if 'helpfunction' in available_options [option ]:
253+ result = available_options [option ]['helpfunction' ]()
254+ if result :
255+ helplines = '\n ' + ' ' .join (result )
256+
257+ return helplines
258+
259+
260+ def get_option_completion (args , available_options ):
261+ args .first () # pop ping
262+ args .first () # pop IP
263+ usedoptionslist = []
264+ while args :
265+ option = args .first () # pop option
266+ matched = complete (option , available_options ) # get option parameters
267+ usedoptionslist .append (option ) # list of used options
268+ # Select options
269+ if not args :
270+ # remove from Possible completions used options
271+ for o in usedoptionslist :
272+ if o in matched :
273+ matched .remove (o )
274+ return ' ' .join (matched )
275+
276+ if len (matched ) > 1 :
277+ return ' ' .join (matched )
278+ # If option doesn't have value
279+ if matched :
280+ if available_options [matched [0 ]]['type' ] == 'noarg' :
281+ continue
282+ else :
283+ # Unexpected option
284+ completion_failure (option )
285+
286+ args .first () # pop option's value
287+ if not args :
288+ matched = complete (option , available_options )
289+ return option_value_help (matched [0 ], available_options )
290+
291+ return ''
292+
293+
294+ def get_icmp_completion (args ):
295+ return get_option_completion (args , ping_options )
296+
297+
298+ def get_tcp_completion (args ):
299+ return get_option_completion (args , tcp_completion_options )
300+
301+
302+ def has_tcp_option (args ):
303+ args = List (args [:])
304+ args .first () # pop ping
305+ args .first () # pop IP
306+
307+ while args :
308+ shortname = args .first ()
309+ longnames = complete (shortname , ping_options )
310+ if len (longnames ) != 1 :
311+ continue
312+
313+ longname = longnames [0 ]
314+ if longname == 'tcp' :
315+ return True
316+
317+ if ping_options [longname ]['type' ] != 'noarg' :
318+ args .first ()
319+
320+ return False
321+
322+
323+ def get_completion (args ):
324+ if has_tcp_option (args ):
325+ return get_tcp_completion (args )
326+
327+ return get_icmp_completion (args )
328+
329+
330+ def tcp_usage (message ):
331+ sys .stderr .write (f'{ message } \n ' )
332+ return 2
333+
334+
335+ def parse_positive_int (value , option ):
336+ try :
337+ number = int (value )
338+ except ValueError :
339+ raise UsageError (f'ping tcp: invalid { option } : { value } ' )
340+
341+ if number < 1 :
342+ raise UsageError (f'ping tcp: invalid { option } : { value } ' )
343+
344+ return number
345+
346+
347+ def parse_tcp_port (value ):
348+ port = parse_positive_int (value , 'port' )
349+ if port > 65535 :
350+ raise UsageError (f'ping tcp: invalid port: { value } ' )
351+
352+ return port
353+
354+
355+ def parse_tcp_args (argv ):
356+ args = List (argv )
357+ host = args .first ()
358+ if not host :
359+ raise UsageError ('ping tcp: Missing host' )
360+
361+ result = {
362+ 'host' : host ,
363+ 'port' : None ,
364+ 'count' : None ,
365+ 'interface' : None ,
366+ 'source_address' : None ,
367+ 'vrf' : 'default' ,
368+ }
369+
370+ while args :
371+ shortname = args .first ()
372+ longnames = complete (shortname , tcp_completion_options )
373+ if len (longnames ) > 1 :
374+ raise UsageError (f'ping tcp: ambiguous option: { shortname } ' )
375+ if not longnames :
376+ raise UsageError (f'ping tcp: invalid option: { shortname } ' )
377+
378+ longname = longnames [0 ]
379+ if longname == 'tcp' :
380+ continue
381+
382+ if not args :
383+ raise UsageError (f'ping tcp: missing argument for { longname } option' )
384+
385+ value = args .first ()
386+ if longname == 'port' :
387+ result ['port' ] = parse_tcp_port (value )
388+ elif longname == 'count' :
389+ result ['count' ] = parse_positive_int (value , 'count' )
390+ elif longname == 'source-address' :
391+ try :
392+ ipaddress .ip_address (value )
393+ except ValueError :
394+ raise UsageError (f'ping tcp: invalid source-address: { value } ' )
395+ result ['source_address' ] = value
396+ elif longname == 'interface' :
397+ result ['interface' ] = value
398+ elif longname == 'vrf' :
399+ result ['vrf' ] = value
400+
401+ if result ['port' ] is None :
402+ raise UsageError ('ping tcp: missing port option' )
403+
404+ return result
405+
406+
407+ def tcp_uses_ipv6 (config ):
408+ addresses = [config ['host' ], config ['source_address' ]]
409+ for address in addresses :
410+ if not address :
411+ continue
412+ try :
413+ if ipaddress .ip_address (address ).version == 6 :
414+ return True
415+ except ValueError :
416+ continue
417+
418+ return False
419+
420+
421+ def build_tcp_nping_command (config ):
422+ command = [
423+ NPING ,
424+ '--tcp-connect' ,
425+ '--dest-port' ,
426+ str (config ['port' ]),
427+ '--count' ,
428+ str (config ['count' ] if config ['count' ] is not None else 0 ),
429+ '--delay' ,
430+ TCP_DELAY ,
431+ ]
432+
433+ if tcp_uses_ipv6 (config ):
434+ command .insert (1 , '-6' )
435+
436+ if config ['interface' ]:
437+ command .extend (['--interface' , config ['interface' ]])
438+
439+ if config ['source_address' ]:
440+ command .extend (['--source-ip' , config ['source_address' ]])
441+
442+ command .append (config ['host' ])
443+
444+ if config ['vrf' ] != 'default' :
445+ return ['sudo' , '/bin/ip' , 'vrf' , 'exec' , config ['vrf' ]] + command
446+
447+ if config ['interface' ] or config ['source_address' ]:
448+ return ['sudo' ] + command
449+
450+ return command
451+
452+
453+ def run_tcp_nping (command , popen = subprocess .Popen ):
454+ successful_connections = None
455+ process = None
456+
457+ try :
458+ process = popen (
459+ command ,
460+ stdout = subprocess .PIPE ,
461+ stderr = subprocess .STDOUT ,
462+ text = True ,
463+ )
464+ for line in process .stdout :
465+ print (line , end = '' )
466+ match = SUCCESSFUL_CONNECTIONS_RE .search (line )
467+ if match :
468+ successful_connections = int (match .group (1 ))
469+ returncode = process .wait ()
470+ except FileNotFoundError :
471+ raise UsageError ('ping tcp: nping is not installed' )
472+ except KeyboardInterrupt :
473+ if process :
474+ process .terminate ()
475+ process .wait ()
476+ print ()
477+ return 130
478+
479+ if successful_connections is not None :
480+ return 0 if successful_connections > 0 else 1
481+
482+ return returncode if returncode else 1
483+
484+
485+ def run_tcp_ping (argv ):
486+ try :
487+ config = parse_tcp_args (argv )
488+ return run_tcp_nping (build_tcp_nping_command (config ))
489+ except UsageError as err :
490+ return tcp_usage (str (err ))
491+
492+
217493if __name__ == '__main__' :
218494 args = List (sys .argv [1 :])
219495 host = args .first ()
@@ -222,44 +498,11 @@ def convert(command, args):
222498 sys .exit ("ping: Missing host" )
223499
224500 if host == '--get-options' :
225- args .first () # pop ping
226- args .first () # pop IP
227- usedoptionslist = []
228- while args :
229- option = args .first () # pop option
230- matched = complete (option ) # get option parameters
231- usedoptionslist .append (option ) # list of used options
232- # Select options
233- if not args :
234- # remove from Possible completions used options
235- for o in usedoptionslist :
236- if o in matched :
237- matched .remove (o )
238- sys .stdout .write (' ' .join (matched ))
239- sys .exit (0 )
240-
241- if len (matched ) > 1 :
242- sys .stdout .write (' ' .join (matched ))
243- sys .exit (0 )
244- # If option doesn't have value
245- if matched :
246- if options [matched [0 ]]['type' ] == 'noarg' :
247- continue
248- else :
249- # Unexpected option
250- completion_failure (option )
251-
252- value = args .first () # pop option's value
253- if not args :
254- matched = complete (option )
255- helplines = options [matched [0 ]]['type' ]
256- # Run helpfunction to get list of possible values
257- if 'helpfunction' in options [matched [0 ]]:
258- result = options [matched [0 ]]['helpfunction' ]()
259- if result :
260- helplines = '\n ' + ' ' .join (result )
261- sys .stdout .write (helplines )
262- sys .exit (0 )
501+ sys .stdout .write (get_completion (args ))
502+ sys .exit (0 )
503+
504+ if has_tcp_option (['ping' , host ] + args ):
505+ sys .exit (run_tcp_ping ([host ] + args ))
263506
264507 for name , option in options .items ():
265508 if 'dflt' in option and name not in args :
0 commit comments