mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
[vnet] feat: add SSH configuration diagnostic (#55594)
* [vnet] feat: add SSH configuration diagnostic This commit adds a VNet diagnostic that reports whether the default user OpenSSH config file (`~/.ssh/config`) includes VNet's generated SSH config file. * fix lint * fix license * Render paths as code, use TextSelectCopy for snippet * add code stying for path * add ssh report to diag story * handle ~/.ssh/config not existing * handle unescaped windows path separators and single quotes * fix typo in comment * Diag report story: Include error when sshConfigAttempt is set to "error" * read ~/.ssh/config as part of the diag instead of abusing cat * fix lint --------- Co-authored-by: Rafał Cieślak <[email protected]>
This commit is contained in:
co-authored by
Rafał Cieślak
parent
57c909dab1
commit
a61a4874c7
+18
-11
@@ -317,21 +317,28 @@ func FullProfilePath(dir string) string {
|
||||
|
||||
// defaultProfilePath retrieves the default path of the TSH profile.
|
||||
func defaultProfilePath() string {
|
||||
// start with UserHomeDir, which is the fastest option as it
|
||||
// relies only on environment variables and does not perform
|
||||
// a user lookup (which can be very slow on large AD environments)
|
||||
home, err := os.UserHomeDir()
|
||||
if err == nil && home != "" {
|
||||
return filepath.Join(home, profileDir)
|
||||
}
|
||||
|
||||
home = os.TempDir()
|
||||
if u, err := user.Current(); err == nil && u.HomeDir != "" {
|
||||
home = u.HomeDir
|
||||
home, ok := UserHomeDir()
|
||||
if !ok {
|
||||
home = os.TempDir()
|
||||
}
|
||||
return filepath.Join(home, profileDir)
|
||||
}
|
||||
|
||||
// UserHomeDir returns the current user's home directory if it can be found.
|
||||
func UserHomeDir() (string, bool) {
|
||||
// Start with os.UserHomeDir, which is the fastest option as it relies only
|
||||
// on environment variables and does not perform a user lookup (which can be
|
||||
// very slow on large AD environments).
|
||||
if home, err := os.UserHomeDir(); err == nil && home != "" {
|
||||
return home, true
|
||||
}
|
||||
// Fall back to the user lookup.
|
||||
if u, err := user.Current(); err == nil && u.HomeDir != "" {
|
||||
return u.HomeDir, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// FromDir reads the user profile from a given directory. If dir is empty,
|
||||
// this function defaults to the default tsh profile directory. If name is empty,
|
||||
// this function defaults to loading the currently active profile (if any).
|
||||
|
||||
@@ -82,9 +82,9 @@ const (
|
||||
// vnetKnownHosts is the file name of the known_hosts file trusted by
|
||||
// third-party SSH clients connecting to VNet SSH.
|
||||
vnetKnownHosts = "vnet_known_hosts"
|
||||
// vnetSSHConfig is the file name of the generated OpenSSH-compatible config
|
||||
// VNetSSHConfig is the file name of the generated OpenSSH-compatible config
|
||||
// file to be used by third-party SSH clients connecting to VNet SSH.
|
||||
vnetSSHConfig = "vnet_ssh_config"
|
||||
VNetSSHConfig = "vnet_ssh_config"
|
||||
)
|
||||
|
||||
// Here's the file layout of all these keypaths.
|
||||
@@ -463,7 +463,7 @@ func VNetKnownHostsPath(baseDir string) string {
|
||||
// VNetSSHConfigPath returns the path to VNet's generated OpenSSH-compatible
|
||||
// config file.
|
||||
func VNetSSHConfigPath(baseDir string) string {
|
||||
return filepath.Join(baseDir, vnetSSHConfig)
|
||||
return filepath.Join(baseDir, VNetSSHConfig)
|
||||
}
|
||||
|
||||
// TrimKeyPathSuffix returns the given path with any key suffix/extension trimmed off.
|
||||
|
||||
@@ -496,6 +496,7 @@ type CheckReport struct {
|
||||
// Types that are valid to be assigned to Report:
|
||||
//
|
||||
// *CheckReport_RouteConflictReport
|
||||
// *CheckReport_SshConfigurationReport
|
||||
Report isCheckReport_Report `protobuf_oneof:"report"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -554,6 +555,15 @@ func (x *CheckReport) GetRouteConflictReport() *RouteConflictReport {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CheckReport) GetSshConfigurationReport() *SSHConfigurationReport {
|
||||
if x != nil {
|
||||
if x, ok := x.Report.(*CheckReport_SshConfigurationReport); ok {
|
||||
return x.SshConfigurationReport
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isCheckReport_Report interface {
|
||||
isCheckReport_Report()
|
||||
}
|
||||
@@ -564,8 +574,15 @@ type CheckReport_RouteConflictReport struct {
|
||||
RouteConflictReport *RouteConflictReport `protobuf:"bytes,2,opt,name=route_conflict_report,json=routeConflictReport,proto3,oneof"`
|
||||
}
|
||||
|
||||
type CheckReport_SshConfigurationReport struct {
|
||||
// ssh_configuration_report reports the status of the system's SSH configuration.
|
||||
SshConfigurationReport *SSHConfigurationReport `protobuf:"bytes,3,opt,name=ssh_configuration_report,json=sshConfigurationReport,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*CheckReport_RouteConflictReport) isCheckReport_Report() {}
|
||||
|
||||
func (*CheckReport_SshConfigurationReport) isCheckReport_Report() {}
|
||||
|
||||
// CommandAttempt describes the attempt at running a particular command associated with a diagnostic
|
||||
// check.
|
||||
type CommandAttempt struct {
|
||||
@@ -761,6 +778,93 @@ func (x *RouteConflict) GetInterfaceApp() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// SSHConfigurationReport describes the state of the system's SSH configuration.
|
||||
type SSHConfigurationReport struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// user_openssh_config_path is the full path to the user's default OpenSSH
|
||||
// config file (~/.ssh/config).
|
||||
UserOpensshConfigPath string `protobuf:"bytes,1,opt,name=user_openssh_config_path,json=userOpensshConfigPath,proto3" json:"user_openssh_config_path,omitempty"`
|
||||
// vnet_ssh_config_path is the path to VNet's generated OpenSSH-compatible
|
||||
// config file.
|
||||
VnetSshConfigPath string `protobuf:"bytes,2,opt,name=vnet_ssh_config_path,json=vnetSshConfigPath,proto3" json:"vnet_ssh_config_path,omitempty"`
|
||||
// user_openssh_config_includes_vnet_ssh_config is true if the default
|
||||
// OpenSSH user configuration file includes VNet's SSH config file.
|
||||
UserOpensshConfigIncludesVnetSshConfig bool `protobuf:"varint,3,opt,name=user_openssh_config_includes_vnet_ssh_config,json=userOpensshConfigIncludesVnetSshConfig,proto3" json:"user_openssh_config_includes_vnet_ssh_config,omitempty"`
|
||||
// user_openssh_config_exists is true if a file exists at
|
||||
// user_openssh_config_path (~/.ssh/config).
|
||||
UserOpensshConfigExists bool `protobuf:"varint,4,opt,name=user_openssh_config_exists,json=userOpensshConfigExists,proto3" json:"user_openssh_config_exists,omitempty"`
|
||||
// user_openssh_config_contents contains the contents of the file at
|
||||
// user_openssh_config_path if it exists.
|
||||
UserOpensshConfigContents string `protobuf:"bytes,5,opt,name=user_openssh_config_contents,json=userOpensshConfigContents,proto3" json:"user_openssh_config_contents,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SSHConfigurationReport) Reset() {
|
||||
*x = SSHConfigurationReport{}
|
||||
mi := &file_teleport_lib_vnet_diag_v1_diag_proto_msgTypes[8]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SSHConfigurationReport) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SSHConfigurationReport) ProtoMessage() {}
|
||||
|
||||
func (x *SSHConfigurationReport) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_teleport_lib_vnet_diag_v1_diag_proto_msgTypes[8]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SSHConfigurationReport.ProtoReflect.Descriptor instead.
|
||||
func (*SSHConfigurationReport) Descriptor() ([]byte, []int) {
|
||||
return file_teleport_lib_vnet_diag_v1_diag_proto_rawDescGZIP(), []int{8}
|
||||
}
|
||||
|
||||
func (x *SSHConfigurationReport) GetUserOpensshConfigPath() string {
|
||||
if x != nil {
|
||||
return x.UserOpensshConfigPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SSHConfigurationReport) GetVnetSshConfigPath() string {
|
||||
if x != nil {
|
||||
return x.VnetSshConfigPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SSHConfigurationReport) GetUserOpensshConfigIncludesVnetSshConfig() bool {
|
||||
if x != nil {
|
||||
return x.UserOpensshConfigIncludesVnetSshConfig
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *SSHConfigurationReport) GetUserOpensshConfigExists() bool {
|
||||
if x != nil {
|
||||
return x.UserOpensshConfigExists
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *SSHConfigurationReport) GetUserOpensshConfigContents() string {
|
||||
if x != nil {
|
||||
return x.UserOpensshConfigContents
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_teleport_lib_vnet_diag_v1_diag_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_teleport_lib_vnet_diag_v1_diag_proto_rawDesc = "" +
|
||||
@@ -785,10 +889,11 @@ const file_teleport_lib_vnet_diag_v1_diag_proto_rawDesc = "" +
|
||||
"\x06status\x18\x01 \x01(\x0e2-.teleport.lib.vnet.diag.v1.CheckAttemptStatusR\x06status\x12\x14\n" +
|
||||
"\x05error\x18\x02 \x01(\tR\x05error\x12I\n" +
|
||||
"\fcheck_report\x18\x03 \x01(\v2&.teleport.lib.vnet.diag.v1.CheckReportR\vcheckReport\x12E\n" +
|
||||
"\bcommands\x18\x04 \x03(\v2).teleport.lib.vnet.diag.v1.CommandAttemptR\bcommands\"\xc3\x01\n" +
|
||||
"\bcommands\x18\x04 \x03(\v2).teleport.lib.vnet.diag.v1.CommandAttemptR\bcommands\"\xb2\x02\n" +
|
||||
"\vCheckReport\x12D\n" +
|
||||
"\x06status\x18\x01 \x01(\x0e2,.teleport.lib.vnet.diag.v1.CheckReportStatusR\x06status\x12d\n" +
|
||||
"\x15route_conflict_report\x18\x02 \x01(\v2..teleport.lib.vnet.diag.v1.RouteConflictReportH\x00R\x13routeConflictReportB\b\n" +
|
||||
"\x15route_conflict_report\x18\x02 \x01(\v2..teleport.lib.vnet.diag.v1.RouteConflictReportH\x00R\x13routeConflictReport\x12m\n" +
|
||||
"\x18ssh_configuration_report\x18\x03 \x01(\v21.teleport.lib.vnet.diag.v1.SSHConfigurationReportH\x00R\x16sshConfigurationReportB\b\n" +
|
||||
"\x06report\"\xa1\x01\n" +
|
||||
"\x0eCommandAttempt\x12G\n" +
|
||||
"\x06status\x18\x01 \x01(\x0e2/.teleport.lib.vnet.diag.v1.CommandAttemptStatusR\x06status\x12\x14\n" +
|
||||
@@ -801,7 +906,13 @@ const file_teleport_lib_vnet_diag_v1_diag_proto_rawDesc = "" +
|
||||
"\x04dest\x18\x01 \x01(\tR\x04dest\x12\x1b\n" +
|
||||
"\tvnet_dest\x18\x02 \x01(\tR\bvnetDest\x12%\n" +
|
||||
"\x0einterface_name\x18\x03 \x01(\tR\rinterfaceName\x12#\n" +
|
||||
"\rinterface_app\x18\x04 \x01(\tR\finterfaceApp*w\n" +
|
||||
"\rinterface_app\x18\x04 \x01(\tR\finterfaceApp\"\xde\x02\n" +
|
||||
"\x16SSHConfigurationReport\x127\n" +
|
||||
"\x18user_openssh_config_path\x18\x01 \x01(\tR\x15userOpensshConfigPath\x12/\n" +
|
||||
"\x14vnet_ssh_config_path\x18\x02 \x01(\tR\x11vnetSshConfigPath\x12\\\n" +
|
||||
",user_openssh_config_includes_vnet_ssh_config\x18\x03 \x01(\bR&userOpensshConfigIncludesVnetSshConfig\x12;\n" +
|
||||
"\x1auser_openssh_config_exists\x18\x04 \x01(\bR\x17userOpensshConfigExists\x12?\n" +
|
||||
"\x1cuser_openssh_config_contents\x18\x05 \x01(\tR\x19userOpensshConfigContents*w\n" +
|
||||
"\x12CheckAttemptStatus\x12$\n" +
|
||||
" CHECK_ATTEMPT_STATUS_UNSPECIFIED\x10\x00\x12\x1b\n" +
|
||||
"\x17CHECK_ATTEMPT_STATUS_OK\x10\x01\x12\x1e\n" +
|
||||
@@ -828,23 +939,24 @@ func file_teleport_lib_vnet_diag_v1_diag_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var file_teleport_lib_vnet_diag_v1_diag_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
|
||||
var file_teleport_lib_vnet_diag_v1_diag_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
|
||||
var file_teleport_lib_vnet_diag_v1_diag_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
|
||||
var file_teleport_lib_vnet_diag_v1_diag_proto_goTypes = []any{
|
||||
(CheckAttemptStatus)(0), // 0: teleport.lib.vnet.diag.v1.CheckAttemptStatus
|
||||
(CheckReportStatus)(0), // 1: teleport.lib.vnet.diag.v1.CheckReportStatus
|
||||
(CommandAttemptStatus)(0), // 2: teleport.lib.vnet.diag.v1.CommandAttemptStatus
|
||||
(*Report)(nil), // 3: teleport.lib.vnet.diag.v1.Report
|
||||
(*NetworkStackAttempt)(nil), // 4: teleport.lib.vnet.diag.v1.NetworkStackAttempt
|
||||
(*NetworkStack)(nil), // 5: teleport.lib.vnet.diag.v1.NetworkStack
|
||||
(*CheckAttempt)(nil), // 6: teleport.lib.vnet.diag.v1.CheckAttempt
|
||||
(*CheckReport)(nil), // 7: teleport.lib.vnet.diag.v1.CheckReport
|
||||
(*CommandAttempt)(nil), // 8: teleport.lib.vnet.diag.v1.CommandAttempt
|
||||
(*RouteConflictReport)(nil), // 9: teleport.lib.vnet.diag.v1.RouteConflictReport
|
||||
(*RouteConflict)(nil), // 10: teleport.lib.vnet.diag.v1.RouteConflict
|
||||
(*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp
|
||||
(CheckAttemptStatus)(0), // 0: teleport.lib.vnet.diag.v1.CheckAttemptStatus
|
||||
(CheckReportStatus)(0), // 1: teleport.lib.vnet.diag.v1.CheckReportStatus
|
||||
(CommandAttemptStatus)(0), // 2: teleport.lib.vnet.diag.v1.CommandAttemptStatus
|
||||
(*Report)(nil), // 3: teleport.lib.vnet.diag.v1.Report
|
||||
(*NetworkStackAttempt)(nil), // 4: teleport.lib.vnet.diag.v1.NetworkStackAttempt
|
||||
(*NetworkStack)(nil), // 5: teleport.lib.vnet.diag.v1.NetworkStack
|
||||
(*CheckAttempt)(nil), // 6: teleport.lib.vnet.diag.v1.CheckAttempt
|
||||
(*CheckReport)(nil), // 7: teleport.lib.vnet.diag.v1.CheckReport
|
||||
(*CommandAttempt)(nil), // 8: teleport.lib.vnet.diag.v1.CommandAttempt
|
||||
(*RouteConflictReport)(nil), // 9: teleport.lib.vnet.diag.v1.RouteConflictReport
|
||||
(*RouteConflict)(nil), // 10: teleport.lib.vnet.diag.v1.RouteConflict
|
||||
(*SSHConfigurationReport)(nil), // 11: teleport.lib.vnet.diag.v1.SSHConfigurationReport
|
||||
(*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp
|
||||
}
|
||||
var file_teleport_lib_vnet_diag_v1_diag_proto_depIdxs = []int32{
|
||||
11, // 0: teleport.lib.vnet.diag.v1.Report.created_at:type_name -> google.protobuf.Timestamp
|
||||
12, // 0: teleport.lib.vnet.diag.v1.Report.created_at:type_name -> google.protobuf.Timestamp
|
||||
4, // 1: teleport.lib.vnet.diag.v1.Report.network_stack_attempt:type_name -> teleport.lib.vnet.diag.v1.NetworkStackAttempt
|
||||
6, // 2: teleport.lib.vnet.diag.v1.Report.checks:type_name -> teleport.lib.vnet.diag.v1.CheckAttempt
|
||||
0, // 3: teleport.lib.vnet.diag.v1.NetworkStackAttempt.status:type_name -> teleport.lib.vnet.diag.v1.CheckAttemptStatus
|
||||
@@ -854,13 +966,14 @@ var file_teleport_lib_vnet_diag_v1_diag_proto_depIdxs = []int32{
|
||||
8, // 7: teleport.lib.vnet.diag.v1.CheckAttempt.commands:type_name -> teleport.lib.vnet.diag.v1.CommandAttempt
|
||||
1, // 8: teleport.lib.vnet.diag.v1.CheckReport.status:type_name -> teleport.lib.vnet.diag.v1.CheckReportStatus
|
||||
9, // 9: teleport.lib.vnet.diag.v1.CheckReport.route_conflict_report:type_name -> teleport.lib.vnet.diag.v1.RouteConflictReport
|
||||
2, // 10: teleport.lib.vnet.diag.v1.CommandAttempt.status:type_name -> teleport.lib.vnet.diag.v1.CommandAttemptStatus
|
||||
10, // 11: teleport.lib.vnet.diag.v1.RouteConflictReport.route_conflicts:type_name -> teleport.lib.vnet.diag.v1.RouteConflict
|
||||
12, // [12:12] is the sub-list for method output_type
|
||||
12, // [12:12] is the sub-list for method input_type
|
||||
12, // [12:12] is the sub-list for extension type_name
|
||||
12, // [12:12] is the sub-list for extension extendee
|
||||
0, // [0:12] is the sub-list for field type_name
|
||||
11, // 10: teleport.lib.vnet.diag.v1.CheckReport.ssh_configuration_report:type_name -> teleport.lib.vnet.diag.v1.SSHConfigurationReport
|
||||
2, // 11: teleport.lib.vnet.diag.v1.CommandAttempt.status:type_name -> teleport.lib.vnet.diag.v1.CommandAttemptStatus
|
||||
10, // 12: teleport.lib.vnet.diag.v1.RouteConflictReport.route_conflicts:type_name -> teleport.lib.vnet.diag.v1.RouteConflict
|
||||
13, // [13:13] is the sub-list for method output_type
|
||||
13, // [13:13] is the sub-list for method input_type
|
||||
13, // [13:13] is the sub-list for extension type_name
|
||||
13, // [13:13] is the sub-list for extension extendee
|
||||
0, // [0:13] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_teleport_lib_vnet_diag_v1_diag_proto_init() }
|
||||
@@ -870,6 +983,7 @@ func file_teleport_lib_vnet_diag_v1_diag_proto_init() {
|
||||
}
|
||||
file_teleport_lib_vnet_diag_v1_diag_proto_msgTypes[4].OneofWrappers = []any{
|
||||
(*CheckReport_RouteConflictReport)(nil),
|
||||
(*CheckReport_SshConfigurationReport)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
@@ -877,7 +991,7 @@ func file_teleport_lib_vnet_diag_v1_diag_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_teleport_lib_vnet_diag_v1_diag_proto_rawDesc), len(file_teleport_lib_vnet_diag_v1_diag_proto_rawDesc)),
|
||||
NumEnums: 3,
|
||||
NumMessages: 8,
|
||||
NumMessages: 9,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
+140
-1
@@ -183,6 +183,14 @@ export interface CheckReport {
|
||||
* @generated from protobuf field: teleport.lib.vnet.diag.v1.RouteConflictReport route_conflict_report = 2;
|
||||
*/
|
||||
routeConflictReport: RouteConflictReport;
|
||||
} | {
|
||||
oneofKind: "sshConfigurationReport";
|
||||
/**
|
||||
* ssh_configuration_report reports the status of the system's SSH configuration.
|
||||
*
|
||||
* @generated from protobuf field: teleport.lib.vnet.diag.v1.SSHConfigurationReport ssh_configuration_report = 3;
|
||||
*/
|
||||
sshConfigurationReport: SSHConfigurationReport;
|
||||
} | {
|
||||
oneofKind: undefined;
|
||||
};
|
||||
@@ -263,6 +271,48 @@ export interface RouteConflict {
|
||||
*/
|
||||
interfaceApp: string;
|
||||
}
|
||||
/**
|
||||
* SSHConfigurationReport describes the state of the system's SSH configuration.
|
||||
*
|
||||
* @generated from protobuf message teleport.lib.vnet.diag.v1.SSHConfigurationReport
|
||||
*/
|
||||
export interface SSHConfigurationReport {
|
||||
/**
|
||||
* user_openssh_config_path is the full path to the user's default OpenSSH
|
||||
* config file (~/.ssh/config).
|
||||
*
|
||||
* @generated from protobuf field: string user_openssh_config_path = 1;
|
||||
*/
|
||||
userOpensshConfigPath: string;
|
||||
/**
|
||||
* vnet_ssh_config_path is the path to VNet's generated OpenSSH-compatible
|
||||
* config file.
|
||||
*
|
||||
* @generated from protobuf field: string vnet_ssh_config_path = 2;
|
||||
*/
|
||||
vnetSshConfigPath: string;
|
||||
/**
|
||||
* user_openssh_config_includes_vnet_ssh_config is true if the default
|
||||
* OpenSSH user configuration file includes VNet's SSH config file.
|
||||
*
|
||||
* @generated from protobuf field: bool user_openssh_config_includes_vnet_ssh_config = 3;
|
||||
*/
|
||||
userOpensshConfigIncludesVnetSshConfig: boolean;
|
||||
/**
|
||||
* user_openssh_config_exists is true if a file exists at
|
||||
* user_openssh_config_path (~/.ssh/config).
|
||||
*
|
||||
* @generated from protobuf field: bool user_openssh_config_exists = 4;
|
||||
*/
|
||||
userOpensshConfigExists: boolean;
|
||||
/**
|
||||
* user_openssh_config_contents contains the contents of the file at
|
||||
* user_openssh_config_path if it exists.
|
||||
*
|
||||
* @generated from protobuf field: string user_openssh_config_contents = 5;
|
||||
*/
|
||||
userOpensshConfigContents: string;
|
||||
}
|
||||
/**
|
||||
* CheckAttemptStatus describes whether CheckAttempt finished successfully. This is different from
|
||||
* CheckReportStatus, which describes whether a successful attempt at running a check has found any
|
||||
@@ -599,7 +649,8 @@ class CheckReport$Type extends MessageType<CheckReport> {
|
||||
constructor() {
|
||||
super("teleport.lib.vnet.diag.v1.CheckReport", [
|
||||
{ no: 1, name: "status", kind: "enum", T: () => ["teleport.lib.vnet.diag.v1.CheckReportStatus", CheckReportStatus, "CHECK_REPORT_STATUS_"] },
|
||||
{ no: 2, name: "route_conflict_report", kind: "message", oneof: "report", T: () => RouteConflictReport }
|
||||
{ no: 2, name: "route_conflict_report", kind: "message", oneof: "report", T: () => RouteConflictReport },
|
||||
{ no: 3, name: "ssh_configuration_report", kind: "message", oneof: "report", T: () => SSHConfigurationReport }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<CheckReport>): CheckReport {
|
||||
@@ -624,6 +675,12 @@ class CheckReport$Type extends MessageType<CheckReport> {
|
||||
routeConflictReport: RouteConflictReport.internalBinaryRead(reader, reader.uint32(), options, (message.report as any).routeConflictReport)
|
||||
};
|
||||
break;
|
||||
case /* teleport.lib.vnet.diag.v1.SSHConfigurationReport ssh_configuration_report */ 3:
|
||||
message.report = {
|
||||
oneofKind: "sshConfigurationReport",
|
||||
sshConfigurationReport: SSHConfigurationReport.internalBinaryRead(reader, reader.uint32(), options, (message.report as any).sshConfigurationReport)
|
||||
};
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === "throw")
|
||||
@@ -642,6 +699,9 @@ class CheckReport$Type extends MessageType<CheckReport> {
|
||||
/* teleport.lib.vnet.diag.v1.RouteConflictReport route_conflict_report = 2; */
|
||||
if (message.report.oneofKind === "routeConflictReport")
|
||||
RouteConflictReport.internalBinaryWrite(message.report.routeConflictReport, writer.tag(2, WireType.LengthDelimited).fork(), options).join();
|
||||
/* teleport.lib.vnet.diag.v1.SSHConfigurationReport ssh_configuration_report = 3; */
|
||||
if (message.report.oneofKind === "sshConfigurationReport")
|
||||
SSHConfigurationReport.internalBinaryWrite(message.report.sshConfigurationReport, writer.tag(3, WireType.LengthDelimited).fork(), options).join();
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false)
|
||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
@@ -841,3 +901,82 @@ class RouteConflict$Type extends MessageType<RouteConflict> {
|
||||
* @generated MessageType for protobuf message teleport.lib.vnet.diag.v1.RouteConflict
|
||||
*/
|
||||
export const RouteConflict = new RouteConflict$Type();
|
||||
// @generated message type with reflection information, may provide speed optimized methods
|
||||
class SSHConfigurationReport$Type extends MessageType<SSHConfigurationReport> {
|
||||
constructor() {
|
||||
super("teleport.lib.vnet.diag.v1.SSHConfigurationReport", [
|
||||
{ no: 1, name: "user_openssh_config_path", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||
{ no: 2, name: "vnet_ssh_config_path", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||
{ no: 3, name: "user_openssh_config_includes_vnet_ssh_config", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
|
||||
{ no: 4, name: "user_openssh_config_exists", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
|
||||
{ no: 5, name: "user_openssh_config_contents", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
|
||||
]);
|
||||
}
|
||||
create(value?: PartialMessage<SSHConfigurationReport>): SSHConfigurationReport {
|
||||
const message = globalThis.Object.create((this.messagePrototype!));
|
||||
message.userOpensshConfigPath = "";
|
||||
message.vnetSshConfigPath = "";
|
||||
message.userOpensshConfigIncludesVnetSshConfig = false;
|
||||
message.userOpensshConfigExists = false;
|
||||
message.userOpensshConfigContents = "";
|
||||
if (value !== undefined)
|
||||
reflectionMergePartial<SSHConfigurationReport>(this, message, value);
|
||||
return message;
|
||||
}
|
||||
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: SSHConfigurationReport): SSHConfigurationReport {
|
||||
let message = target ?? this.create(), end = reader.pos + length;
|
||||
while (reader.pos < end) {
|
||||
let [fieldNo, wireType] = reader.tag();
|
||||
switch (fieldNo) {
|
||||
case /* string user_openssh_config_path */ 1:
|
||||
message.userOpensshConfigPath = reader.string();
|
||||
break;
|
||||
case /* string vnet_ssh_config_path */ 2:
|
||||
message.vnetSshConfigPath = reader.string();
|
||||
break;
|
||||
case /* bool user_openssh_config_includes_vnet_ssh_config */ 3:
|
||||
message.userOpensshConfigIncludesVnetSshConfig = reader.bool();
|
||||
break;
|
||||
case /* bool user_openssh_config_exists */ 4:
|
||||
message.userOpensshConfigExists = reader.bool();
|
||||
break;
|
||||
case /* string user_openssh_config_contents */ 5:
|
||||
message.userOpensshConfigContents = reader.string();
|
||||
break;
|
||||
default:
|
||||
let u = options.readUnknownField;
|
||||
if (u === "throw")
|
||||
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||
let d = reader.skip(wireType);
|
||||
if (u !== false)
|
||||
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}
|
||||
internalBinaryWrite(message: SSHConfigurationReport, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||
/* string user_openssh_config_path = 1; */
|
||||
if (message.userOpensshConfigPath !== "")
|
||||
writer.tag(1, WireType.LengthDelimited).string(message.userOpensshConfigPath);
|
||||
/* string vnet_ssh_config_path = 2; */
|
||||
if (message.vnetSshConfigPath !== "")
|
||||
writer.tag(2, WireType.LengthDelimited).string(message.vnetSshConfigPath);
|
||||
/* bool user_openssh_config_includes_vnet_ssh_config = 3; */
|
||||
if (message.userOpensshConfigIncludesVnetSshConfig !== false)
|
||||
writer.tag(3, WireType.Varint).bool(message.userOpensshConfigIncludesVnetSshConfig);
|
||||
/* bool user_openssh_config_exists = 4; */
|
||||
if (message.userOpensshConfigExists !== false)
|
||||
writer.tag(4, WireType.Varint).bool(message.userOpensshConfigExists);
|
||||
/* string user_openssh_config_contents = 5; */
|
||||
if (message.userOpensshConfigContents !== "")
|
||||
writer.tag(5, WireType.LengthDelimited).string(message.userOpensshConfigContents);
|
||||
let u = options.writeUnknownFields;
|
||||
if (u !== false)
|
||||
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||
return writer;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @generated MessageType for protobuf message teleport.lib.vnet.diag.v1.SSHConfigurationReport
|
||||
*/
|
||||
export const SSHConfigurationReport = new SSHConfigurationReport$Type();
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -29,6 +30,8 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/gravitational/teleport"
|
||||
"github.com/gravitational/teleport/api/profile"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
prehogv1alpha "github.com/gravitational/teleport/gen/proto/go/prehog/v1alpha"
|
||||
apiteleterm "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1"
|
||||
api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/vnet/v1"
|
||||
@@ -90,6 +93,7 @@ type Config struct {
|
||||
// reporting.
|
||||
InstallationID string
|
||||
Clock clockwork.Clock
|
||||
profilePath string
|
||||
}
|
||||
|
||||
// CheckAndSetDefaults checks and sets the defaults
|
||||
@@ -110,6 +114,10 @@ func (c *Config) CheckAndSetDefaults() error {
|
||||
c.Clock = clockwork.NewRealClock()
|
||||
}
|
||||
|
||||
if c.profilePath == "" {
|
||||
c.profilePath = profile.FullProfilePath(os.Getenv(types.HomeEnvVar))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -62,10 +62,20 @@ func (s *Service) RunDiagnostics(ctx context.Context, req *api.RunDiagnosticsReq
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
sshDiag, err := diag.NewSSHDiag(&diag.SSHConfig{
|
||||
ProfilePath: s.cfg.profilePath,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
|
||||
report, err := diag.GenerateReport(ctx, diag.ReportPrerequisites{
|
||||
Clock: s.cfg.Clock,
|
||||
NetworkStackAttempt: nsa,
|
||||
DiagChecks: []diag.DiagCheck{routeConflictDiag},
|
||||
DiagChecks: []diag.DiagCheck{
|
||||
routeConflictDiag,
|
||||
sshDiag,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
// Teleport
|
||||
// Copyright (C) 2025 Gravitational, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package diag
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"github.com/gravitational/teleport/api/profile"
|
||||
"github.com/gravitational/teleport/api/utils/keypaths"
|
||||
diagv1 "github.com/gravitational/teleport/gen/proto/go/teleport/lib/vnet/diag/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
maxOpenSSHConfigFileSize = 1 * 1024 * 1024 // 1 MiB
|
||||
)
|
||||
|
||||
// SSHConfig includes everything that [SSHDiag] needs to run.
|
||||
type SSHConfig struct {
|
||||
// ProfilePath is the path to the user profile (TELEPORT_HOME) where VNet's
|
||||
// SSH configuration file is stored.
|
||||
ProfilePath string
|
||||
}
|
||||
|
||||
// SSHDiag is a diagnostic check that inspects whether the default user OpenSSH
|
||||
// config file includes VNet's generated SSH config file.
|
||||
type SSHDiag struct {
|
||||
cfg *SSHConfig
|
||||
userHome string
|
||||
userOpenSSHConfigPath string
|
||||
vnetSSHConfigPath string
|
||||
isWindows bool
|
||||
}
|
||||
|
||||
// NewSSHDiag returns a new [SSHDiag].
|
||||
func NewSSHDiag(cfg *SSHConfig) (*SSHDiag, error) {
|
||||
userHome, ok := profile.UserHomeDir()
|
||||
if !ok {
|
||||
return nil, trace.Errorf("unable to find user's home directory")
|
||||
}
|
||||
userOpenSSHConfigPath := filepath.Join(userHome, ".ssh", "config")
|
||||
vnetSSHConfigPath := filepath.Join(cfg.ProfilePath, keypaths.VNetSSHConfig)
|
||||
return &SSHDiag{
|
||||
cfg: cfg,
|
||||
userHome: userHome,
|
||||
userOpenSSHConfigPath: userOpenSSHConfigPath,
|
||||
vnetSSHConfigPath: vnetSSHConfigPath,
|
||||
isWindows: runtime.GOOS == "windows",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Commands returns no commands for this diagnostic.
|
||||
func (d *SSHDiag) Commands(ctx context.Context) []*exec.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// EmptyCheckReport returns an empty SSH configuration report.
|
||||
func (d *SSHDiag) EmptyCheckReport() *diagv1.CheckReport {
|
||||
return &diagv1.CheckReport{Report: &diagv1.CheckReport_SshConfigurationReport{}}
|
||||
}
|
||||
|
||||
// Run runs the diagnostic.
|
||||
func (d *SSHDiag) Run(ctx context.Context) (*diagv1.CheckReport, error) {
|
||||
report, err := d.run(ctx)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err)
|
||||
}
|
||||
return &diagv1.CheckReport{
|
||||
// This intentionally always returns CHECK_REPORT_STATUS_OK even if
|
||||
// ~/.ssh/config does not include the VNet generated SSH config. It is
|
||||
// not mandatory to configure SSH and returning an error status would
|
||||
// cause an alert and notification in Connect.
|
||||
Status: diagv1.CheckReportStatus_CHECK_REPORT_STATUS_OK,
|
||||
Report: &diagv1.CheckReport_SshConfigurationReport{
|
||||
SshConfigurationReport: report,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *SSHDiag) run(ctx context.Context) (*diagv1.SSHConfigurationReport, error) {
|
||||
_, err := os.Stat(d.userOpenSSHConfigPath)
|
||||
userOpenSSHConfigExists := err == nil
|
||||
if !userOpenSSHConfigExists {
|
||||
return &diagv1.SSHConfigurationReport{
|
||||
UserOpensshConfigPath: d.userOpenSSHConfigPath,
|
||||
VnetSshConfigPath: d.vnetSSHConfigPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
userOpenSSHConfigFile, err := os.Open(d.userOpenSSHConfigPath)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(trace.ConvertSystemError(err), "opening %s for reading", d.userOpenSSHConfigPath)
|
||||
}
|
||||
defer userOpenSSHConfigFile.Close()
|
||||
|
||||
userOpenSSHConfigContents, err := io.ReadAll(io.LimitReader(userOpenSSHConfigFile, maxOpenSSHConfigFileSize))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(trace.ConvertSystemError(err), "reading %s", d.userOpenSSHConfigPath)
|
||||
}
|
||||
if len(userOpenSSHConfigContents) == maxOpenSSHConfigFileSize {
|
||||
return nil, trace.Errorf("%s is too large to (max size %s)",
|
||||
d.userOpenSSHConfigPath, humanize.Bytes(maxOpenSSHConfigFileSize))
|
||||
}
|
||||
if !utf8.Valid(userOpenSSHConfigContents) {
|
||||
return nil, trace.Errorf("%s is not valid UTF-8", d.userOpenSSHConfigPath)
|
||||
}
|
||||
|
||||
included, err := d.openSSHConfigIncludesVNetSSHConfig(bytes.NewReader(userOpenSSHConfigContents))
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "checking if the default user OpenSSH config includes VNet's SSH configuration")
|
||||
}
|
||||
return &diagv1.SSHConfigurationReport{
|
||||
UserOpensshConfigPath: d.userOpenSSHConfigPath,
|
||||
VnetSshConfigPath: d.vnetSSHConfigPath,
|
||||
UserOpensshConfigIncludesVnetSshConfig: included,
|
||||
UserOpensshConfigExists: true,
|
||||
UserOpensshConfigContents: string(userOpenSSHConfigContents),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *SSHDiag) openSSHConfigIncludesVNetSSHConfig(r io.Reader) (bool, error) {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
if d.openSSHConfigLineIncludesPath(scanner.Text(), d.vnetSSHConfigPath) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, trace.Wrap(scanner.Err())
|
||||
}
|
||||
|
||||
// openSSHConfigLineIncludesPath returns true if the given line of an OpenSSH
|
||||
// configuration file is an include statement for the given path.
|
||||
func (d *SSHDiag) openSSHConfigLineIncludesPath(line, wantPath string) bool {
|
||||
wantPath = d.normalizePath(wantPath)
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Only consider lines that begin with "include" (case-insensitive).
|
||||
i := strings.IndexFunc(line, isSpace)
|
||||
if i == -1 {
|
||||
return false
|
||||
}
|
||||
if strings.ToLower(line[:i]) != "include" {
|
||||
return false
|
||||
}
|
||||
// Consider the rest of the line after "include".
|
||||
line = line[i+1:]
|
||||
|
||||
// Include lines may specify multiple pathnames and each pathname may
|
||||
// contain glob wildcards, tokens, environment variables, ~, escaped
|
||||
// characters and may or may not be quoted. This function does not support
|
||||
// glob wildcards, tokens, or environment variables. It splits each argument
|
||||
// at unescaped and unquoted whitespace and if the argument matches wantPath
|
||||
// returns true. It does support ~ as an alias for the user's home
|
||||
// directory.
|
||||
var (
|
||||
// b is a running buffer holding the current argument as parsed up to
|
||||
// the current point.
|
||||
b strings.Builder
|
||||
// quote holds the opening quote character if one has been found.
|
||||
quote = byte(0)
|
||||
)
|
||||
loop:
|
||||
for i := 0; i < len(line); i++ {
|
||||
c := line[i]
|
||||
switch {
|
||||
case c == '\\' && i < len(line)-1 && canBeEscaped(line[i+1]):
|
||||
// Skip the escape char and write the next char literally.
|
||||
i++
|
||||
b.WriteByte(line[i])
|
||||
case quote == 0 && (c == '"' || c == '\''):
|
||||
// Start of quote
|
||||
quote = c
|
||||
case quote != 0 && c == quote:
|
||||
// End of quote
|
||||
quote = 0
|
||||
case b.Len() == 0 && c == '~':
|
||||
// Support ~ as an alias for the user's home directory.
|
||||
b.WriteString(d.userHome)
|
||||
case quote == 0 && c == '#':
|
||||
// Found an unquoted comment in the middle of the line, ignore the rest.
|
||||
break loop
|
||||
case quote == 0 && isSpace(rune(c)):
|
||||
// Reached the end of this argument, check if it matches wantPath.
|
||||
if d.normalizePath(b.String()) == wantPath {
|
||||
return true
|
||||
}
|
||||
b.Reset()
|
||||
default:
|
||||
// By default just append the current character to the current
|
||||
// argument.
|
||||
b.WriteByte(c)
|
||||
}
|
||||
}
|
||||
if quote != 0 {
|
||||
// Unmatched quote.
|
||||
return false
|
||||
}
|
||||
// Handle an argument that ends at the end of the line.
|
||||
return d.normalizePath(b.String()) == wantPath
|
||||
}
|
||||
|
||||
func (d *SSHDiag) normalizePath(path string) string {
|
||||
if d.isWindows {
|
||||
// Normalize all paths to use unix-style separators since OpenSSH
|
||||
// supports / or \\ on Windows.
|
||||
path = strings.ReplaceAll(path, `\`, `/`)
|
||||
// Windows paths are case-insensitive.
|
||||
path = strings.ToLower(path)
|
||||
}
|
||||
return filepath.Clean(path)
|
||||
}
|
||||
|
||||
func isSpace(r rune) bool {
|
||||
switch r {
|
||||
case ' ', '\t':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func canBeEscaped(c byte) bool {
|
||||
// https://github.com/openssh/openssh-portable/blob/5f761cdb2331a12318bde24db5ca84ee144a51d1/misc.c#L2089-L2099
|
||||
switch c {
|
||||
case ' ', '\\', '\'', '"':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// Teleport
|
||||
// Copyright (C) 2025 Gravitational, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package diag
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gravitational/teleport/api/utils/keypaths"
|
||||
diagv1 "github.com/gravitational/teleport/gen/proto/go/teleport/lib/vnet/diag/v1"
|
||||
)
|
||||
|
||||
// TestSSHDiag tests the SSH configuration diagnostic, specifically its ability
|
||||
// to check whether an OpenSSH config file includes the VNet SSH config file.
|
||||
func TestSSHDiag(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, tc := range []struct {
|
||||
desc string
|
||||
profilePath string
|
||||
userHome string
|
||||
isWindows bool
|
||||
input string
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
desc: "empty",
|
||||
profilePath: `/Users/user/.tsh`,
|
||||
userHome: `/Users/user`,
|
||||
},
|
||||
{
|
||||
desc: "macos tsh",
|
||||
profilePath: `/Users/user/.tsh`,
|
||||
userHome: `/Users/user`,
|
||||
input: `Include /Users/user/.tsh/vnet_ssh_config`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "macos tsh ~",
|
||||
profilePath: `/Users/user/.tsh`,
|
||||
userHome: `/Users/user`,
|
||||
input: `Include ~/.tsh/vnet_ssh_config`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "macos connect",
|
||||
profilePath: `/Users/user/Application Support/Teleport Connect/tsh`,
|
||||
userHome: `/Users/user`,
|
||||
input: `Include "/Users/user/Application Support/Teleport Connect/tsh/vnet_ssh_config"`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "macos connect ~",
|
||||
profilePath: `/Users/user/Application Support/Teleport Connect/tsh`,
|
||||
userHome: `/Users/user`,
|
||||
input: `Include "~/Application Support/Teleport Connect/tsh/vnet_ssh_config"`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "macos tsh not match connect",
|
||||
profilePath: `/Users/user/.tsh`,
|
||||
userHome: `/Users/user`,
|
||||
input: `Include "/Users/user/Application Support/Teleport Connect/tsh/vnet_ssh_config"`,
|
||||
},
|
||||
{
|
||||
desc: "macos connect not match tsh",
|
||||
profilePath: `/Users/user/Application Support/Teleport Connect/tsh`,
|
||||
userHome: `/Users/user`,
|
||||
input: `Include /Users/user/.tsh/vnet_ssh_config`,
|
||||
},
|
||||
{
|
||||
desc: "windows tsh",
|
||||
profilePath: `C:\Users\User\.tsh`,
|
||||
userHome: `C:\Users\User`,
|
||||
isWindows: true,
|
||||
input: `Include "C:\\Users\\User\\.tsh\\vnet_ssh_config"`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "windows tsh unescaped",
|
||||
profilePath: `C:\Users\User\.tsh`,
|
||||
userHome: `C:\Users\User`,
|
||||
isWindows: true,
|
||||
input: `Include "C:\Users\User\.tsh\vnet_ssh_config"`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "windows tsh unix path",
|
||||
profilePath: `C:\Users\User\.tsh`,
|
||||
userHome: `C:\Users\User`,
|
||||
isWindows: true,
|
||||
input: `Include "C:/Users/User/.tsh/vnet_ssh_config"`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "windows tsh ~",
|
||||
profilePath: `C:\Users\User\.tsh`,
|
||||
userHome: `C:\Users\User`,
|
||||
isWindows: true,
|
||||
input: `Include "~\\.tsh\\vnet_ssh_config"`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "windows connect",
|
||||
profilePath: `C:\Users\User\AppData\Roaming\Teleport Connect\tsh`,
|
||||
userHome: `C:\Users\User`,
|
||||
isWindows: true,
|
||||
input: `Include "C:\\Users\\User\\AppData\\Roaming\\Teleport\ Connect\\tsh\\vnet_ssh_config"`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "windows connect unescaped",
|
||||
profilePath: `C:\Users\User\AppData\Roaming\Teleport Connect\tsh`,
|
||||
userHome: `C:\Users\User`,
|
||||
isWindows: true,
|
||||
input: `Include "C:\Users\User\AppData\Roaming\Teleport Connect\tsh\vnet_ssh_config"`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "windows connect unix path",
|
||||
profilePath: `C:\Users\User\AppData\Roaming\Teleport Connect\tsh`,
|
||||
userHome: `C:\Users\User`,
|
||||
isWindows: true,
|
||||
input: `Include "C:/Users/User/AppData/Roaming/Teleport\ Connect/tsh/vnet_ssh_config"`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "windows connect ~",
|
||||
profilePath: `C:\Users\User\AppData\Roaming\Teleport Connect\tsh`,
|
||||
userHome: `C:\Users\User`,
|
||||
isWindows: true,
|
||||
input: `Include "~\\AppData\\Roaming\\Teleport\ Connect\\tsh\\vnet_ssh_config"`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "windows tsh not match connect",
|
||||
profilePath: `C:\Users\User\.tsh`,
|
||||
userHome: `C:\Users\User`,
|
||||
isWindows: true,
|
||||
input: `Include "C:\\Users\\User\\AppData\\Roaming\\Teleport\ Connect\\tsh\\vnet_ssh_config"`,
|
||||
},
|
||||
{
|
||||
desc: "windows connect not match tsh",
|
||||
profilePath: `C:\Users\User\AppData\Roaming\Teleport Connect\tsh`,
|
||||
userHome: `C:\Users\User`,
|
||||
isWindows: true,
|
||||
input: `Include "C:\\Users\\User\\.tsh\\vnet_ssh_config"`,
|
||||
},
|
||||
{
|
||||
desc: "some other file",
|
||||
profilePath: `/Users/user/.tsh`,
|
||||
input: `Include /Users/user/.tsh/ssh_config`,
|
||||
},
|
||||
{
|
||||
desc: "multiple includes",
|
||||
profilePath: `/Users/user/.tsh`,
|
||||
userHome: `/Users/user`,
|
||||
input: `
|
||||
Include ~/.ssh/include/*
|
||||
Include /Users/user/ssh_config
|
||||
Include /Users/user/.tsh/vnet_ssh_config
|
||||
`,
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
desc: "commented",
|
||||
profilePath: `/Users/user/.tsh`,
|
||||
userHome: `/Users/user`,
|
||||
input: `Include #/Users/user/.tsh/vnet_ssh_config`,
|
||||
},
|
||||
{
|
||||
desc: "single quotes",
|
||||
profilePath: `/Users/user/.tsh`,
|
||||
userHome: `/Users/user`,
|
||||
input: `Include '/Users/user/.tsh/vnet_ssh_config'`,
|
||||
expect: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
diag, err := NewSSHDiag(&SSHConfig{
|
||||
ProfilePath: tc.profilePath,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
userOpenSSHConfigPath := filepath.Join(t.TempDir(), "test_ssh_config")
|
||||
|
||||
// Override isWindows and paths for the purpose of the test.
|
||||
diag.isWindows = tc.isWindows
|
||||
diag.userHome = tc.userHome
|
||||
diag.userOpenSSHConfigPath = userOpenSSHConfigPath
|
||||
|
||||
if len(tc.input) > 0 {
|
||||
require.NoError(t, os.WriteFile(userOpenSSHConfigPath, []byte(tc.input), 0o600))
|
||||
}
|
||||
|
||||
expectReport := &diagv1.CheckReport{
|
||||
Status: diagv1.CheckReportStatus_CHECK_REPORT_STATUS_OK,
|
||||
Report: &diagv1.CheckReport_SshConfigurationReport{
|
||||
SshConfigurationReport: &diagv1.SSHConfigurationReport{
|
||||
UserOpensshConfigPath: userOpenSSHConfigPath,
|
||||
VnetSshConfigPath: keypaths.VNetSSHConfigPath(tc.profilePath),
|
||||
UserOpensshConfigIncludesVnetSshConfig: tc.expect,
|
||||
UserOpensshConfigExists: len(tc.input) > 0,
|
||||
UserOpensshConfigContents: tc.input,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
report, err := diag.Run(t.Context())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectReport, report)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -107,6 +107,8 @@ message CheckReport {
|
||||
// route_conflict reports whether there are routes that might conflict with routes set up by
|
||||
// VNet.
|
||||
RouteConflictReport route_conflict_report = 2;
|
||||
// ssh_configuration_report reports the status of the system's SSH configuration.
|
||||
SSHConfigurationReport ssh_configuration_report = 3;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,3 +160,22 @@ message RouteConflict {
|
||||
// it's likely to be empty.
|
||||
string interface_app = 4;
|
||||
}
|
||||
|
||||
// SSHConfigurationReport describes the state of the system's SSH configuration.
|
||||
message SSHConfigurationReport {
|
||||
// user_openssh_config_path is the full path to the user's default OpenSSH
|
||||
// config file (~/.ssh/config).
|
||||
string user_openssh_config_path = 1;
|
||||
// vnet_ssh_config_path is the path to VNet's generated OpenSSH-compatible
|
||||
// config file.
|
||||
string vnet_ssh_config_path = 2;
|
||||
// user_openssh_config_includes_vnet_ssh_config is true if the default
|
||||
// OpenSSH user configuration file includes VNet's SSH config file.
|
||||
bool user_openssh_config_includes_vnet_ssh_config = 3;
|
||||
// user_openssh_config_exists is true if a file exists at
|
||||
// user_openssh_config_path (~/.ssh/config).
|
||||
bool user_openssh_config_exists = 4;
|
||||
// user_openssh_config_contents contains the contents of the file at
|
||||
// user_openssh_config_path if it exists.
|
||||
string user_openssh_config_contents = 5;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { WindowsDesktop } from 'gen-proto-ts/teleport/lib/teleterm/v1/windows_de
|
||||
import {
|
||||
CheckReport,
|
||||
RouteConflictReport,
|
||||
SSHConfigurationReport,
|
||||
} from 'gen-proto-ts/teleport/lib/vnet/diag/v1/diag_pb';
|
||||
|
||||
import {
|
||||
@@ -194,3 +195,12 @@ export function reportOneOfIsRouteConflictReport(
|
||||
} {
|
||||
return report.oneofKind === 'routeConflictReport';
|
||||
}
|
||||
|
||||
export function reportOneOfIsSSHConfigurationReport(
|
||||
report: CheckReport['report']
|
||||
): report is {
|
||||
oneofKind: 'sshConfigurationReport';
|
||||
sshConfigurationReport: SSHConfigurationReport;
|
||||
} {
|
||||
return report.oneofKind === 'sshConfigurationReport';
|
||||
}
|
||||
|
||||
@@ -33,5 +33,21 @@ default link#25 UCSIg utun4
|
||||
100.100.100.100 link#25 UHWIi utun4
|
||||
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
⚠️ VNet SSH is not configured.
|
||||
|
||||
The user's default SSH configuration file does not include VNet's
|
||||
generated configuration file and connections to VNet SSH hosts will
|
||||
not work by default.
|
||||
|
||||
| File description | Path |
|
||||
| ------------------------ | ---- |
|
||||
| User OpenSSH config file | ~/.ssh/config |
|
||||
| VNet SSH config file | /Users/user/Library/Application Support/Teleport Connect/tsh/vnet_ssh_config |
|
||||
|
||||
~/.ssh/config does not exist
|
||||
|
||||
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -29,10 +29,10 @@ import {
|
||||
|
||||
describe('reportToText', () => {
|
||||
it('converts report correctly', () => {
|
||||
const checkReport = makeCheckReport({
|
||||
const routeConflictReport = makeCheckReport({
|
||||
status: diag.CheckReportStatus.ISSUES_FOUND,
|
||||
});
|
||||
checkReport.report = {
|
||||
routeConflictReport.report = {
|
||||
oneofKind: 'routeConflictReport',
|
||||
routeConflictReport: {
|
||||
routeConflicts: [
|
||||
@@ -51,12 +51,29 @@ describe('reportToText', () => {
|
||||
],
|
||||
},
|
||||
};
|
||||
const sshConfigReport = makeCheckReport({
|
||||
status: diag.CheckReportStatus.OK,
|
||||
});
|
||||
sshConfigReport.report = {
|
||||
oneofKind: 'sshConfigurationReport',
|
||||
sshConfigurationReport: {
|
||||
userOpensshConfigPath: '~/.ssh/config',
|
||||
vnetSshConfigPath:
|
||||
'/Users/user/Library/Application Support/Teleport Connect/tsh/vnet_ssh_config',
|
||||
userOpensshConfigIncludesVnetSshConfig: false,
|
||||
userOpensshConfigExists: false,
|
||||
userOpensshConfigContents: '',
|
||||
},
|
||||
};
|
||||
const report = makeReport({
|
||||
checks: [
|
||||
makeCheckAttempt({
|
||||
checkReport,
|
||||
checkReport: routeConflictReport,
|
||||
commands: [makeCommandAttempt()],
|
||||
}),
|
||||
makeCheckAttempt({
|
||||
checkReport: sshConfigReport,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,10 @@ import { displayDateTime } from 'design/datetime';
|
||||
import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb';
|
||||
import * as diag from 'gen-proto-ts/teleport/lib/vnet/diag/v1/diag_pb';
|
||||
|
||||
import { reportOneOfIsRouteConflictReport } from 'teleterm/helpers';
|
||||
import {
|
||||
reportOneOfIsRouteConflictReport,
|
||||
reportOneOfIsSSHConfigurationReport,
|
||||
} from 'teleterm/helpers';
|
||||
|
||||
export const hasReportFoundIssues = (report: diag.Report): boolean =>
|
||||
report.checks.some(
|
||||
@@ -122,6 +125,10 @@ const reportOneofToDisplayDetails: Record<
|
||||
errorTitle: 'inspect network routes',
|
||||
reportToText: routeConflictReportToText,
|
||||
},
|
||||
sshConfigurationReport: {
|
||||
errorTitle: 'inspect SSH configuration',
|
||||
reportToText: sshConfigurationReportToText,
|
||||
},
|
||||
};
|
||||
|
||||
function routeConflictReportToText({
|
||||
@@ -149,3 +156,43 @@ function routeConflictReportToText({
|
||||
| ---------------- | ----------------------- | --------- | --------- |
|
||||
${tableRows}`;
|
||||
}
|
||||
|
||||
function sshConfigurationReportToText({ report }: diag.CheckReport): string {
|
||||
if (!reportOneOfIsSSHConfigurationReport(report)) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
userOpensshConfigPath,
|
||||
vnetSshConfigPath,
|
||||
userOpensshConfigIncludesVnetSshConfig,
|
||||
userOpensshConfigExists,
|
||||
userOpensshConfigContents,
|
||||
} = report.sshConfigurationReport;
|
||||
|
||||
const status = userOpensshConfigIncludesVnetSshConfig
|
||||
? '✅ VNet SSH is configured correctly.'
|
||||
: `⚠️ VNet SSH is not configured.
|
||||
|
||||
The user's default SSH configuration file does not include VNet's
|
||||
generated configuration file and connections to VNet SSH hosts will
|
||||
not work by default.`;
|
||||
|
||||
const pathsTable = `
|
||||
| File description | Path |
|
||||
| ------------------------ | ---- |
|
||||
| User OpenSSH config file | ${userOpensshConfigPath} |
|
||||
| VNet SSH config file | ${vnetSshConfigPath} |`;
|
||||
|
||||
const currentContents = userOpensshConfigExists
|
||||
? `Current contents of ${userOpensshConfigPath}:
|
||||
|
||||
\`\`\`
|
||||
${userOpensshConfigContents}
|
||||
\`\`\``
|
||||
: `${userOpensshConfigPath} does not exist`;
|
||||
|
||||
return `${status}
|
||||
${pathsTable}
|
||||
|
||||
${currentContents}`;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
CommandAttemptStatus,
|
||||
RouteConflict,
|
||||
RouteConflictReport,
|
||||
SSHConfigurationReport,
|
||||
} from 'gen-proto-ts/teleport/lib/vnet/diag/v1/diag_pb';
|
||||
import { usePromiseRejectedOnUnmount } from 'shared/utils/wait';
|
||||
|
||||
@@ -48,6 +49,12 @@ import { ConnectionsContextProvider } from 'teleterm/ui/TopBar/Connections/conne
|
||||
import { DocumentVnetDiagReport as Component } from './DocumentVnetDiagReport';
|
||||
import { useVnetContext, VnetContextProvider } from './vnetContext';
|
||||
|
||||
const defaultUserSSHConfigContents = `Include "/Users/User/Library/Application Support/Teleport Connect/tsh/vnet_ssh_config"
|
||||
|
||||
Host github.com
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
`;
|
||||
|
||||
type StoryProps = {
|
||||
asText: boolean;
|
||||
networkStackAttempt: 'ok' | 'error';
|
||||
@@ -56,6 +63,10 @@ type StoryProps = {
|
||||
routeConflictAttempt: 'ok' | 'issues-found' | 'error';
|
||||
routeConflicts: RouteConflict[];
|
||||
routeConflictCommandAttempt: 'ok' | 'error';
|
||||
sshConfigAttempt: 'ok' | 'error';
|
||||
sshConfigured: boolean;
|
||||
userOpenSSHConfigExists: boolean;
|
||||
userOpenSSHConfigContents: string;
|
||||
displayUnsupportedCheckAttempt: boolean;
|
||||
vnetRunning: boolean;
|
||||
reRunDiagnostics: 'success' | 'error' | 'processing';
|
||||
@@ -91,6 +102,15 @@ const meta: Meta<StoryProps> = {
|
||||
control: { type: 'inline-radio' },
|
||||
options: ['ok', 'error'],
|
||||
},
|
||||
sshConfigAttempt: {
|
||||
control: { type: 'inline-radio' },
|
||||
options: ['ok', 'error'],
|
||||
},
|
||||
userOpenSSHConfigExists: {},
|
||||
userOpenSSHConfigContents: {},
|
||||
sshConfigured: {
|
||||
control: { type: 'boolean' },
|
||||
},
|
||||
displayUnsupportedCheckAttempt: {
|
||||
description:
|
||||
"Simulate the component receiving a report with a check attempt that's not supported in the current version",
|
||||
@@ -121,6 +141,10 @@ const meta: Meta<StoryProps> = {
|
||||
}),
|
||||
],
|
||||
routeConflictCommandAttempt: 'ok',
|
||||
sshConfigAttempt: 'ok',
|
||||
sshConfigured: false,
|
||||
userOpenSSHConfigExists: true,
|
||||
userOpenSSHConfigContents: defaultUserSSHConfigContents,
|
||||
displayUnsupportedCheckAttempt: false,
|
||||
vnetRunning: true,
|
||||
reRunDiagnostics: 'success',
|
||||
@@ -208,6 +232,31 @@ export function DocumentVnetDiagReport(props: StoryProps) {
|
||||
}
|
||||
report.checks.push(routeConflictCheckAttempt);
|
||||
|
||||
const sshConfigReport: SSHConfigurationReport = {
|
||||
userOpensshConfigIncludesVnetSshConfig: props.sshConfigured,
|
||||
userOpensshConfigPath: '/Users/User/.ssh/config',
|
||||
vnetSshConfigPath:
|
||||
'/Users/User/Library/Application Support/Teleport Connect/tsh/vnet_ssh_config',
|
||||
userOpensshConfigExists: props.userOpenSSHConfigExists,
|
||||
userOpensshConfigContents: props.userOpenSSHConfigContents,
|
||||
};
|
||||
const sshConfigCheckAttempt = makeCheckAttempt({
|
||||
status:
|
||||
props.sshConfigAttempt === 'ok'
|
||||
? CheckAttemptStatus.OK
|
||||
: CheckAttemptStatus.ERROR,
|
||||
error:
|
||||
props.sshConfigAttempt === 'error' ? 'something went wrong' : undefined,
|
||||
checkReport: makeCheckReport({
|
||||
status: CheckReportStatus.OK,
|
||||
report: {
|
||||
oneofKind: 'sshConfigurationReport',
|
||||
sshConfigurationReport: sshConfigReport,
|
||||
},
|
||||
}),
|
||||
});
|
||||
report.checks.push(sshConfigCheckAttempt);
|
||||
|
||||
if (props.displayUnsupportedCheckAttempt) {
|
||||
report.checks.push({
|
||||
status: CheckAttemptStatus.OK,
|
||||
|
||||
@@ -21,7 +21,7 @@ import styled from 'styled-components';
|
||||
|
||||
import { Button, Alert as DesignAlert, Flex, H1, Link, Stack } from 'design';
|
||||
import { AlertProps } from 'design/Alert/Alert';
|
||||
import Table, { TextCell } from 'design/DataTable';
|
||||
import Table, { Cell, TextCell } from 'design/DataTable';
|
||||
import { displayDateTime } from 'design/datetime';
|
||||
import {
|
||||
Copy,
|
||||
@@ -35,10 +35,14 @@ import { HoverTooltip } from 'design/Tooltip';
|
||||
import { copyToClipboard } from 'design/utils/copyToClipboard';
|
||||
import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb';
|
||||
import * as diag from 'gen-proto-ts/teleport/lib/vnet/diag/v1/diag_pb';
|
||||
import { TextSelectCopy } from 'shared/components/TextSelectCopy';
|
||||
import { CanceledError, useAsync } from 'shared/hooks/useAsync';
|
||||
import { pluralize } from 'shared/utils/text';
|
||||
|
||||
import { reportOneOfIsRouteConflictReport } from 'teleterm/helpers';
|
||||
import {
|
||||
reportOneOfIsRouteConflictReport,
|
||||
reportOneOfIsSSHConfigurationReport,
|
||||
} from 'teleterm/helpers';
|
||||
import { getReportFilename, reportToText } from 'teleterm/services/vnet/diag';
|
||||
import { useAppContext } from 'teleterm/ui/appContextProvider';
|
||||
import Document from 'teleterm/ui/Document';
|
||||
@@ -297,6 +301,10 @@ const reportOneofDisplayDetails: Record<
|
||||
errorTitle: 'inspect network routes',
|
||||
Component: CheckReportRouteConflict,
|
||||
},
|
||||
sshConfigurationReport: {
|
||||
errorTitle: 'inspect SSH configuration',
|
||||
Component: CheckReportSSHConfiguration,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -367,6 +375,91 @@ function CheckReportRouteConflict({
|
||||
);
|
||||
}
|
||||
|
||||
function CheckReportSSHConfiguration({
|
||||
checkReport: { report },
|
||||
}: {
|
||||
checkReport: diag.CheckReport;
|
||||
}) {
|
||||
if (!reportOneOfIsSSHConfigurationReport(report)) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
userOpensshConfigPath,
|
||||
vnetSshConfigPath,
|
||||
userOpensshConfigIncludesVnetSshConfig,
|
||||
userOpensshConfigExists,
|
||||
userOpensshConfigContents,
|
||||
} = report.sshConfigurationReport;
|
||||
const pathsTable = (
|
||||
<Table
|
||||
emptyText=""
|
||||
data={[
|
||||
{
|
||||
desc: 'User OpenSSH config file',
|
||||
path: userOpensshConfigPath,
|
||||
},
|
||||
{
|
||||
desc: 'VNet SSH config file',
|
||||
path: vnetSshConfigPath,
|
||||
},
|
||||
]}
|
||||
columns={[
|
||||
{ key: 'desc', headerText: 'File description' },
|
||||
{
|
||||
key: 'path',
|
||||
headerText: 'Path',
|
||||
render: row => (
|
||||
<Cell>
|
||||
<code>{row.path}</code>
|
||||
</Cell>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
if (userOpensshConfigIncludesVnetSshConfig) {
|
||||
return (
|
||||
<>
|
||||
<Stack>
|
||||
<P1>
|
||||
<Success /> VNet SSH is configured correctly.
|
||||
</P1>
|
||||
<P2>
|
||||
The user's default SSH configuration file correctly includes VNet's
|
||||
generated configuration file.
|
||||
</P2>
|
||||
</Stack>
|
||||
{pathsTable}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Stack>
|
||||
<P1>
|
||||
<Warning /> VNet SSH is not configured.
|
||||
</P1>
|
||||
<P2 m={0}>
|
||||
The user's default SSH configuration file does not include VNet's
|
||||
generated SSH configuration file. SSH clients will not be able to make
|
||||
connections to VNet SSH addresses by default. Add the following line
|
||||
to <code>{userOpensshConfigPath}</code> to configure
|
||||
OpenSSH-compatible clients for VNet:
|
||||
</P2>
|
||||
<TextSelectCopy text={`Include "${vnetSshConfigPath}"`} bash={false} />
|
||||
</Stack>
|
||||
{userOpensshConfigExists ? (
|
||||
<details>
|
||||
<Summary>
|
||||
Current contents of <code>{userOpensshConfigPath}</code>
|
||||
</Summary>
|
||||
<Pre>{userOpensshConfigContents}</Pre>
|
||||
</details>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const Summary = styled.summary`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user