44#include <stdarg.h>
55#include <stdlib.h>
66#include <string.h>
7+ #include <fcntl.h>
8+ #include <sys/stat.h>
79#include <sys/types.h>
810#include <sys/wait.h>
911
@@ -79,6 +81,51 @@ static int run(char *const argv[])
7981 return -1 ;
8082}
8183
84+ /*
85+ * Like run(), but drops privileges to the given user before exec.
86+ * Use for commands that must run as the CLI user, not root (klishd).
87+ */
88+ static int run_as_user (const char * user , char * const argv [])
89+ {
90+ struct passwd * pw ;
91+ pid_t pid ;
92+ int rc ;
93+
94+ pw = getpwnam (user );
95+ if (!pw ) {
96+ fprintf (stderr , ERRMSG "unknown user: %s\n" , user );
97+ return -1 ;
98+ }
99+
100+ pid = fork ();
101+ if (pid == -1 )
102+ return -1 ;
103+
104+ if (!pid ) {
105+ if (setgid (pw -> pw_gid ) || setuid (pw -> pw_uid )) {
106+ fprintf (stderr , "Aborting, failed dropping privileges to "
107+ "(UID:%d GID:%d): %s\n" ,
108+ pw -> pw_uid , pw -> pw_gid , strerror (errno ));
109+ _exit (1 );
110+ }
111+ execvp (argv [0 ], argv );
112+ _exit (127 );
113+ }
114+
115+ while (waitpid (pid , & rc , 0 ) != pid )
116+ ;
117+
118+ if (WIFEXITED (rc ))
119+ return WEXITSTATUS (rc );
120+
121+ if (WIFSIGNALED (rc )) {
122+ errno = EINTR ;
123+ return -1 ;
124+ }
125+
126+ return -1 ;
127+ }
128+
82129/*
83130 * Shell command execution - only use with hardcoded commands or when
84131 * shell features (pipes, redirects) are needed. Never use with user input.
@@ -427,6 +474,192 @@ int infix_asym_keys(kcontext_t *ctx)
427474 "| jq -r '.\"ietf-keystore:keystore\".\"asymmetric-keys\".\"asymmetric-key\"[].name'" );
428475}
429476
477+ /*
478+ * Create ~/.ssh/known_hosts with correct ownership if it doesn't exist.
479+ * Must be called as root before dropping privileges, since ~/.ssh/ is
480+ * owned by root on Infix (the system manages authorized_keys via YANG).
481+ * Once the file exists with the user's ownership, ssh(1) can update it.
482+ */
483+ static void ensure_known_hosts (const struct passwd * pw )
484+ {
485+ char path [512 ];
486+ int fd ;
487+
488+ snprintf (path , sizeof (path ), "%s/.ssh/known_hosts" , pw -> pw_dir );
489+ fd = open (path , O_CREAT | O_EXCL | O_WRONLY , 0600 );
490+ if (fd < 0 )
491+ return ; /* Already exists, or unrecoverable error */
492+
493+ fchown (fd , pw -> pw_uid , pw -> pw_gid );
494+ close (fd );
495+ }
496+
497+ int infix_ssh_connect (kcontext_t * ctx )
498+ {
499+ kpargv_t * pargv = kcontext_pargv (ctx );
500+ const char * host , * ruser , * port , * user ;
501+ struct passwd * pw ;
502+ kparg_t * parg ;
503+ char * argv [8 ];
504+ int i = 0 ;
505+
506+ host = kparg_value (kpargv_find (pargv , "host" ));
507+
508+ parg = kpargv_find (pargv , "user" );
509+ ruser = parg ? kparg_value (parg ) : NULL ;
510+
511+ parg = kpargv_find (pargv , "port" );
512+ port = parg ? kparg_value (parg ) : NULL ;
513+
514+ if (!host ) {
515+ fprintf (stderr , ERRMSG "missing host argument.\n" );
516+ return -1 ;
517+ }
518+
519+ user = cd_home (ctx );
520+ pw = getpwnam (user );
521+ if (pw )
522+ ensure_known_hosts (pw );
523+
524+ argv [i ++ ] = "ssh" ;
525+ if (ruser ) { argv [i ++ ] = "-l" ; argv [i ++ ] = (char * )ruser ; }
526+ if (port ) { argv [i ++ ] = "-p" ; argv [i ++ ] = (char * )port ; }
527+ argv [i ++ ] = (char * )host ;
528+ argv [i ] = NULL ;
529+
530+ return run_as_user (user , argv );
531+ }
532+
533+ /*
534+ * Completion: list hostnames from the current user's ~/.ssh/known_hosts.
535+ */
536+ int infix_ssh_known_hosts (kcontext_t * ctx )
537+ {
538+ char path [512 ], line [4096 ];
539+ const char * user ;
540+ struct passwd * pw ;
541+ FILE * f ;
542+
543+ user = cd_home (ctx );
544+ pw = getpwnam (user );
545+ if (!pw )
546+ return 0 ;
547+
548+ snprintf (path , sizeof (path ), "%s/.ssh/known_hosts" , pw -> pw_dir );
549+ f = fopen (path , "r" );
550+ if (!f )
551+ return 0 ;
552+
553+ while (fgets (line , sizeof (line ), f )) {
554+ char * sp ;
555+
556+ /* Skip comments, blank lines, and hashed entries */
557+ if (line [0 ] == '#' || line [0 ] == '\n' || line [0 ] == '|' )
558+ continue ;
559+ sp = strchr (line , ' ' );
560+ if (!sp )
561+ continue ;
562+ * sp = '\0' ;
563+ /* Entries may be comma-separated: "host,ip algo key" */
564+ for (char * tok = strtok (line , "," ); tok ; tok = strtok (NULL , "," ))
565+ puts (tok );
566+ }
567+
568+ fclose (f );
569+ return 0 ;
570+ }
571+
572+ /*
573+ * Pre-enroll a host public key received out-of-band into ~/.ssh/known_hosts.
574+ * Runs as the CLI user to ensure correct file ownership.
575+ */
576+ int infix_ssh_add_known_host (kcontext_t * ctx )
577+ {
578+ kpargv_t * pargv = kcontext_pargv (ctx );
579+ const char * host , * keytype , * pubkey , * user ;
580+ char path [512 ];
581+ struct passwd * pw ;
582+ pid_t pid ;
583+ int rc ;
584+
585+ host = kparg_value (kpargv_find (pargv , "host" ));
586+ keytype = kparg_value (kpargv_find (pargv , "keytype" ));
587+ pubkey = kparg_value (kpargv_find (pargv , "pubkey" ));
588+ if (!host || !keytype || !pubkey ) {
589+ fprintf (stderr , ERRMSG "missing arguments.\n" );
590+ return -1 ;
591+ }
592+
593+ user = cd_home (ctx );
594+ pw = getpwnam (user );
595+ if (!pw ) {
596+ fprintf (stderr , ERRMSG "unknown user: %s\n" , user );
597+ return -1 ;
598+ }
599+
600+ snprintf (path , sizeof (path ), "%s/.ssh/known_hosts" , pw -> pw_dir );
601+
602+ ensure_known_hosts (pw );
603+
604+ pid = fork ();
605+ if (pid == -1 )
606+ return -1 ;
607+
608+ if (!pid ) {
609+ FILE * f ;
610+
611+ if (setgid (pw -> pw_gid ) || setuid (pw -> pw_uid )) {
612+ fprintf (stderr , "Aborting, failed dropping privileges: %s\n" ,
613+ strerror (errno ));
614+ _exit (1 );
615+ }
616+
617+ f = fopen (path , "a" );
618+ if (!f ) {
619+ fprintf (stderr , ERRMSG "cannot open %s: %s\n" , path , strerror (errno ));
620+ _exit (1 );
621+ }
622+
623+ fprintf (f , "%s %s %s\n" , host , keytype , pubkey );
624+ fclose (f );
625+ printf ("Host %s added to %s\n" , host , path );
626+ _exit (0 );
627+ }
628+
629+ while (waitpid (pid , & rc , 0 ) != pid )
630+ ;
631+
632+ if (WIFEXITED (rc ))
633+ return WEXITSTATUS (rc );
634+
635+ if (WIFSIGNALED (rc )) {
636+ errno = EINTR ;
637+ return -1 ;
638+ }
639+
640+ return -1 ;
641+ }
642+
643+ int infix_ssh_remove_known_host (kcontext_t * ctx )
644+ {
645+ kpargv_t * pargv = kcontext_pargv (ctx );
646+ const char * host ;
647+ char * argv [4 ];
648+
649+ host = kparg_value (kpargv_find (pargv , "host" ));
650+ if (!host ) {
651+ fprintf (stderr , ERRMSG "missing host argument.\n" );
652+ return -1 ;
653+ }
654+
655+ argv [0 ] = "ssh-keygen" ;
656+ argv [1 ] = "-R" ;
657+ argv [2 ] = (char * )host ;
658+ argv [3 ] = NULL ;
659+
660+ return run_as_user (cd_home (ctx ), argv );
661+ }
662+
430663int kplugin_infix_fini (kcontext_t * ctx )
431664{
432665 (void )ctx ;
@@ -453,6 +686,10 @@ int kplugin_infix_init(kcontext_t *ctx)
453686 kplugin_add_syms (plugin , ksym_new ("firewall_services" , infix_firewall_services ));
454687 kplugin_add_syms (plugin , ksym_new ("set_boot_order" , infix_set_boot_order ));
455688 kplugin_add_syms (plugin , ksym_new ("shell" , infix_shell ));
689+ kplugin_add_syms (plugin , ksym_new ("ssh_connect" , infix_ssh_connect ));
690+ kplugin_add_syms (plugin , ksym_new ("ssh_known_hosts" , infix_ssh_known_hosts ));
691+ kplugin_add_syms (plugin , ksym_new ("ssh_add_known_host" , infix_ssh_add_known_host ));
692+ kplugin_add_syms (plugin , ksym_new ("ssh_remove_known_host" , infix_ssh_remove_known_host ));
456693
457694 return 0 ;
458695}
0 commit comments