From a4e1cfa010b5f1f1b5485ef98134e267eb2906be Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Tue, 20 Feb 2024 13:09:17 +0100 Subject: [PATCH] Detect GUID Encoding based on reasonable timestamp (#59) --- go.mod | 8 +- go.sum | 20 ++-- internal/uuid-endianness/uuid.go | 100 +++++++++--------- internal/uuid-endianness/uuid_test.go | 93 ++++------------ internal/vendors/supermicro/supermicro.go | 13 +-- .../vendors/supermicro/supermicro_test.go | 41 ------- 6 files changed, 85 insertions(+), 190 deletions(-) diff --git a/go.mod b/go.mod index 166b188..4261c8d 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,12 @@ require ( github.com/avast/retry-go/v4 v4.5.1 github.com/creack/pty v1.1.21 github.com/gliderlabs/ssh v0.3.6 - github.com/google/uuid v1.5.0 + github.com/google/uuid v1.6.0 github.com/sethvargo/go-password v0.2.0 github.com/stmcginnis/gofish v0.15.0 github.com/stretchr/testify v1.8.4 github.com/vmware/goipmi v0.0.0-20181114221114-2333cd82d702 - golang.org/x/net v0.20.0 + golang.org/x/net v0.21.0 ) require ( @@ -19,8 +19,8 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.18.0 // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 6ddb4cd..413d5de 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gliderlabs/ssh v0.3.6 h1:ZzjlDa05TcFRICb3anf/dSPN3ewz1Zx6CMLPWgkm3b8= github.com/gliderlabs/ssh v0.3.6/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -28,14 +28,14 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/vmware/goipmi v0.0.0-20181114221114-2333cd82d702 h1:yx587LNBbOpIxzCBHBiI94Wx8ryIAFlu1w0lDwm64cA= github.com/vmware/goipmi v0.0.0-20181114221114-2333cd82d702/go.mod h1:YiWonbS/PuCtti3wt9jl+FvNEJ7c0nvmjGoEYxdjyk0= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/uuid-endianness/uuid.go b/internal/uuid-endianness/uuid.go index 2bd9b99..a38e398 100644 --- a/internal/uuid-endianness/uuid.go +++ b/internal/uuid-endianness/uuid.go @@ -4,14 +4,16 @@ import ( "bytes" "encoding/binary" "fmt" - guuid "github.com/google/uuid" + "time" + + "github.com/google/uuid" ) -// UuidSize is the size in bytes of a UUID object -const UuidSize = 16 +// uuidSize is the size in bytes of a UUID object +const uuidSize = 16 -// Uuid represents a UUID object as defined by RFC 4122. -type Uuid struct { +// uid represents a UUID object as defined by RFC 4122. +type uid struct { TimeLow uint32 TimeMid uint16 TimeHiAndVersion uint16 @@ -20,65 +22,61 @@ type Uuid struct { Node [6]byte } -// String implements the fmt.Stringer interface -func (u Uuid) String() string { - return fmt.Sprintf("%08x-%04x-%04x-%02x%02x-%x", u.TimeLow, u.TimeMid, u.TimeHiAndVersion, u.ClockSeqHiAndRes, u.ClockSeqLow, u.Node) -} - -func FromString(s string) (Uuid, error) { - var uid Uuid - u, err := guuid.Parse(s) +// Redfish API and probably the DHCP Request return the GUID of a machine encoded either +// in middleEndianBytes +// or as normal string +// +// To detect which one is actually in use, we extract the creation time of the uuid, +// if this time is somehow reasonable, we return the string of the GUID as we got it, +// we need to convert it to mixed endian +// https://en.wikipedia.org/wiki/Universally_unique_identifier#Encoding +func Convert(s string) (string, error) { + u, err := uuid.Parse(s) if err != nil { - return uid, err + return "", err + } + if isNotMixedEncoded(u) { + return u.String(), nil } + b, err := u.MarshalBinary() if err != nil { - return uid, err + return "", err } - return FromBytes(b) -} - -// ToMiddleEndian encodes the UUID into a middle-endian UUID. -// -// A middle-endian encoded UUID represents a UUID where the first -// three groups are Little Endian encoded, while the rest of the -// groups are Big Endian encoded. -func (u Uuid) ToMiddleEndian() (Uuid, error) { - buf, err := u.MiddleEndianBytes() + uid, err := fromBytes(b) if err != nil { - return Uuid{}, err + return "", err } - - return FromBytes(buf) -} - -// BigEndianBytes returns the UUID encoded in Big Endian. -func (u Uuid) BigEndianBytes() ([]byte, error) { - buf := bytes.NewBuffer(make([]byte, 0, UuidSize)) - - err := binary.Write(buf, binary.BigEndian, u) + buf, err := uid.middleEndianBytes() if err != nil { - return nil, err + return "", err } - - return buf.Bytes(), nil + uid, err = fromBytes(buf) + return fmt.Sprintf("%08x-%04x-%04x-%02x%02x-%x", uid.TimeLow, uid.TimeMid, uid.TimeHiAndVersion, uid.ClockSeqHiAndRes, uid.ClockSeqLow, uid.Node), err } -// LittleEndianBytes returns the UUID encoded in Little Endian. -func (u Uuid) LittleEndianBytes() ([]byte, error) { - buf := bytes.NewBuffer(make([]byte, 0, UuidSize)) +// thirtyYears is an educated guess for a plausible time stored in the uuid. +// RFC states that the time is stored in 100s of nanos since 15 Okt 1582 as defined in RFC 4122 +// We check if this time is not more than 30 years apart from now. +// Also if the encoded time is in the future +// If the uid returned from the BMC is mixedEndian encoded, the time extracted is usually in the year 4000 or so. +const thirtyYears = 30 * 365 * 24 * time.Hour - err := binary.Write(buf, binary.LittleEndian, u) - if err != nil { - return nil, err +func isNotMixedEncoded(u uuid.UUID) bool { + if u.Version() != 1 { + return false } - - return buf.Bytes(), nil + uuidTime := time.Unix(u.Time().UnixTime()) + if time.Time(uuidTime).Year() > time.Now().Year() { + return false + } + timeDistance := time.Since(uuidTime).Abs() + return timeDistance < thirtyYears } -// MiddleEndianBytes returns the UUID encoded in Middle Endian. -func (u Uuid) MiddleEndianBytes() ([]byte, error) { - buf := bytes.NewBuffer(make([]byte, 0, UuidSize)) +// middleEndianBytes returns the UUID encoded in Middle Endian. +func (u uid) middleEndianBytes() ([]byte, error) { + buf := bytes.NewBuffer(make([]byte, 0, uuidSize)) if err := binary.Write(buf, binary.LittleEndian, u.TimeLow); err != nil { return nil, err @@ -108,8 +106,8 @@ func (u Uuid) MiddleEndianBytes() ([]byte, error) { } // UuidFromBytes reads a UUID from a given byte slice. -func FromBytes(buf []byte) (Uuid, error) { - var u Uuid +func fromBytes(buf []byte) (uid, error) { + var u uid reader := bytes.NewReader(buf) err := binary.Read(reader, binary.BigEndian, &u) diff --git a/internal/uuid-endianness/uuid_test.go b/internal/uuid-endianness/uuid_test.go index 3b23516..88bfcc0 100644 --- a/internal/uuid-endianness/uuid_test.go +++ b/internal/uuid-endianness/uuid_test.go @@ -2,13 +2,14 @@ package uuid import ( "bytes" - "reflect" "testing" "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func ConvertUUID(s string) []byte { +func convertUUID(s string) []byte { u := uuid.MustParse(s) b, _ := u.MarshalBinary() return b @@ -28,19 +29,19 @@ func TestUuid(t *testing.T) { expect: []byte{0x68, 0x5, 0x34, 0x99, 0x75, 0xf7, 0xe7, 0x11, 0x8c, 0x3f, 0x9a, 0x21, 0x4c, 0xf0, 0x93, 0xae}, // 68053499-75f7-e711-8c3f-9a214cf093ae }, { - value: ConvertUUID("00ecd471-0771-e911-8000-efbeaddeefbe"), - expect: ConvertUUID("71d4ec00-7107-11e9-8000-efbeaddeefbe"), + value: convertUUID("00ecd471-0771-e911-8000-efbeaddeefbe"), + expect: convertUUID("71d4ec00-7107-11e9-8000-efbeaddeefbe"), }, } for i := range tests { tt := tests[i] - value, err := FromBytes(tt.value) + value, err := fromBytes(tt.value) if err != nil { t.Fatalf("Cannot parse UUID %+v: %s", tt.value, err) } - got, err := value.MiddleEndianBytes() + got, err := value.middleEndianBytes() if err != nil { t.Fatalf("Cannot convert to middle endian %+v: %s", value, err) } @@ -51,73 +52,17 @@ func TestUuid(t *testing.T) { } } -func TestFromString(t *testing.T) { - tests := []struct { - name string - input string - wantErr bool - }{ - { - name: "creates uuid", - input: "00ecd471-0771-e911-8000-efbeaddeefbe", - wantErr: false, - }, - { - name: "creates uuid", - input: "11ecd471-2771-e911-8333-efbeaddeefbe", - wantErr: false, - }, - { - name: "creates uuid", - input: "11ecd471-2771-e911-8333-0000addeefbe", - wantErr: false, - }, - } - for i := range tests { - tt := tests[i] - t.Run(tt.name, func(t *testing.T) { - got, err := FromString(tt.input) - if (err != nil) != tt.wantErr { - t.Errorf("FromString() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got.String(), tt.input) { - t.Errorf("FromString() = %v, want %v", got.String(), tt.input) - } - }) - } -} +func TestUUIDConvert(t *testing.T) { + bad := "0060c2bd-3089-eb11-8000-7cc255106b08" + good := "bdc26000-8930-11eb-8000-7cc255106b08" -func TestUuid_ToMiddleEndian(t *testing.T) { - tests := []struct { - name string - input string - want string - wantErr bool - }{ - { - name: "convert to mixed endian", - input: "00ecd471-0771-e911-8000-efbeaddeefbe", - want: "71d4ec00-7107-11e9-8000-efbeaddeefbe", - wantErr: false, - }, - } - for i := range tests { - tt := tests[i] - t.Run(tt.name, func(t *testing.T) { - u, err := FromString(tt.input) - if err != nil { - t.Errorf("Uuid.FormatString() error = %v", err) - return - } - got, err := u.ToMiddleEndian() - if (err != nil) != tt.wantErr { - t.Errorf("Uuid.ToMiddleEndian() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got.String(), tt.want) { - t.Errorf("Uuid.ToMiddleEndian() = %v, want %v", got.String(), tt.want) - } - }) - } + result, err := Convert(good) + require.NoError(t, err) + + assert.Equal(t, good, result) + + result, err = Convert(bad) + require.NoError(t, err) + + assert.Equal(t, good, result) } diff --git a/internal/vendors/supermicro/supermicro.go b/internal/vendors/supermicro/supermicro.go index 491a407..7a25ac7 100644 --- a/internal/vendors/supermicro/supermicro.go +++ b/internal/vendors/supermicro/supermicro.go @@ -193,20 +193,13 @@ func (ob *outBand) UUID() (*uuid.UUID, error) { return &us, nil } - // Redfish returns the UUID in the wrong byte order - // we need to convert it to mixed endian - // https://en.wikipedia.org/wiki/Universally_unique_identifier#Encoding - raw, err := uuidendian.FromString(u) + // Redfish returns the UUID sometimes in mixed encoding + uid, err := uuidendian.Convert(u) if err != nil { return nil, err } - mixed, err := raw.ToMiddleEndian() - if err != nil { - return nil, err - } - - us, err := uuid.Parse(mixed.String()) + us, err := uuid.Parse(uid) if err != nil { return nil, err } diff --git a/internal/vendors/supermicro/supermicro_test.go b/internal/vendors/supermicro/supermicro_test.go index 89fdf4e..72372ff 100644 --- a/internal/vendors/supermicro/supermicro_test.go +++ b/internal/vendors/supermicro/supermicro_test.go @@ -3,50 +3,9 @@ package supermicro import ( "testing" - "github.com/google/uuid" "github.com/metal-stack/go-hal" - uuidendian "github.com/metal-stack/go-hal/internal/uuid-endianness" - "github.com/stretchr/testify/require" ) -func Test_UUID(t *testing.T) { - // given - u := "f6157800-70e3-11e9-8000-efbeaddeefbe" - - uid, err := uuidendian.FromString(u) - - // then - require.NoError(t, err) - require.NotNil(t, uid) - require.Equal(t, "f6157800-70e3-11e9-8000-efbeaddeefbe", uid.String()) - - // when - id, err := UUID(u) - - // then - require.NoError(t, err) - require.NotNil(t, id) - require.Equal(t, "007815f6-e370-e911-8000-efbeaddeefbe", id.String()) -} - -func UUID(u string) (*uuid.UUID, error) { - raw, err := uuidendian.FromString(u) - if err != nil { - return nil, err - } - - mixed, err := raw.ToMiddleEndian() - if err != nil { - return nil, err - } - - us, err := uuid.Parse(mixed.String()) - if err != nil { - return nil, err - } - return &us, nil -} - func Test_inBand_PowerOff(t *testing.T) { type fields struct { sum *sum