From 0e7e10e372e6cba6bd74f1e8ab9a8b8e9d773ff3 Mon Sep 17 00:00:00 2001 From: chaehni Date: Wed, 31 Jul 2019 13:10:45 +0200 Subject: [PATCH] HP: Add hidden path group structure (#2931) Fixes #2889 --- go/lib/ctrl/path_mgmt/hp_cfg.go | 2 +- go/lib/hiddenpath/BUILD.bazel | 26 +++ go/lib/hiddenpath/group.go | 184 ++++++++++++++++++++ go/lib/hiddenpath/group_test.go | 298 ++++++++++++++++++++++++++++++++ 4 files changed, 509 insertions(+), 1 deletion(-) create mode 100644 go/lib/hiddenpath/BUILD.bazel create mode 100644 go/lib/hiddenpath/group.go create mode 100644 go/lib/hiddenpath/group_test.go diff --git a/go/lib/ctrl/path_mgmt/hp_cfg.go b/go/lib/ctrl/path_mgmt/hp_cfg.go index 5eed37a41c..bb7299ac98 100644 --- a/go/lib/ctrl/path_mgmt/hp_cfg.go +++ b/go/lib/ctrl/path_mgmt/hp_cfg.go @@ -42,7 +42,7 @@ var _ proto.Cerealizable = (*HPCfg)(nil) type HPCfg struct { GroupId *HPGroupId Version uint32 - OwnerISD uint16 + OwnerISD addr.ISD Writers []addr.IAInt Readers []addr.IAInt Registries []addr.IAInt diff --git a/go/lib/hiddenpath/BUILD.bazel b/go/lib/hiddenpath/BUILD.bazel new file mode 100644 index 0000000000..bf39753bdc --- /dev/null +++ b/go/lib/hiddenpath/BUILD.bazel @@ -0,0 +1,26 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["group.go"], + importpath = "github.com/scionproto/scion/go/lib/hiddenpath", + visibility = ["//visibility:public"], + deps = [ + "//go/lib/addr:go_default_library", + "//go/lib/common:go_default_library", + "//go/lib/ctrl/path_mgmt:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["group_test.go"], + embed = [":go_default_library"], + deps = [ + "//go/lib/addr:go_default_library", + "//go/lib/ctrl/path_mgmt:go_default_library", + "//go/lib/xtest:go_default_library", + "@com_github_stretchr_testify//assert:go_default_library", + "@com_github_stretchr_testify//require:go_default_library", + ], +) diff --git a/go/lib/hiddenpath/group.go b/go/lib/hiddenpath/group.go new file mode 100644 index 0000000000..f7b61196e9 --- /dev/null +++ b/go/lib/hiddenpath/group.go @@ -0,0 +1,184 @@ +package hiddenpath + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/scionproto/scion/go/lib/addr" + "github.com/scionproto/scion/go/lib/common" + "github.com/scionproto/scion/go/lib/ctrl/path_mgmt" +) + +// Parsing errors +const ( + // InvalidGroupIdFormat indicates an invalid GroupId format + InvalidGroupIdFormat = "Invalid GroupId format" + // InvalidGroupIdSuffix indicates an invalid GroupId suffix + InvalidGroupIdSuffix = "Invalid GroupId suffix" +) + +// Validation errors +const ( + // MissingGroupID indicates a missing GroupId + MissingGroupId = "Missing GroupId" + // InvalidVersion indicates a missing version + InvalidVersion = "Invalid version" + // MissingOwner indicates a missing Owner + MissingOwner = "Missing Owner" + // OwnerMismatch indicates a mismatch between Owner and GroupId.OwnerAS + OwnerMismatch = "Owner mismatch" + // EmptyWriters indicatres an empty Writer section + EmptyWriters = "Writer section cannot be empty" + // EmptyWriters indicatres an empty Writer section + EmptyRegistries = "Registry section cannot be empty" +) + +type GroupId struct { + OwnerAS addr.AS + Suffix uint16 +} + +func (id *GroupId) UnmarshalJSON(data []byte) (err error) { + var v string + if err = json.Unmarshal(data, &v); err != nil { + return err + } + parts := strings.Split(v, "-") + if len(parts) != 2 { + return common.NewBasicError(InvalidGroupIdFormat, nil, "GroupId", v) + } + ownerAS, err := addr.ASFromString(parts[0]) + if err != nil { + return err + } + suffix, err := strconv.ParseUint(parts[1], 16, 16) + if err != nil { + return common.NewBasicError(InvalidGroupIdSuffix, err, "Suffix", parts[1]) + } + id.OwnerAS = ownerAS + id.Suffix = uint16(suffix) + return nil +} + +func (id GroupId) MarshalJSON() ([]byte, error) { + return json.Marshal(fmt.Sprintf("%s-%x", id.OwnerAS, id.Suffix)) +} + +type Group struct { + Id GroupId `json:"GroupID"` + Version uint + Owner addr.IA + Writers []addr.IA + Readers []addr.IA + Registries []addr.IA +} + +func (h *Group) UnmarshalJSON(data []byte) (err error) { + type groupAlias Group + var v groupAlias + if err = json.Unmarshal(data, &v); err != nil { + return err + } + if v.Id == (GroupId{}) { + return common.NewBasicError(MissingGroupId, nil) + } + if v.Version == 0 { + return common.NewBasicError(InvalidVersion, nil) + } + if v.Owner == (addr.IA{}) { + return common.NewBasicError(MissingOwner, nil) + } + if v.Owner.A != v.Id.OwnerAS { + return common.NewBasicError(OwnerMismatch, nil, + "OwnerAS", v.Owner.A, "GroupId.OwnerAS", v.Id.OwnerAS) + } + if len(v.Writers) == 0 { + return common.NewBasicError(EmptyWriters, nil) + } + if len(v.Registries) == 0 { + return common.NewBasicError(EmptyRegistries, nil) + } + *h = Group(v) + return nil +} + +// HasWriter returns true if ia is a Writer of h +func (h Group) HasWriter(ia addr.IA) bool { + for _, w := range h.Writers { + if w == ia { + return true + } + } + return false +} + +// HasReader returns true if ia is a Reader of h +func (h Group) HasReader(ia addr.IA) bool { + for _, r := range h.Readers { + if r == ia { + return true + } + } + return false +} + +// HasRegistry returns true if ia is a Registry of h +func (h Group) HasRegistry(ia addr.IA) bool { + for _, r := range h.Registries { + if r == ia { + return true + } + } + return false +} + +// ToMsg returns h as Cerializable message suitable to be sent via messenger +func (h Group) ToMsg() *path_mgmt.HPCfg { + return &path_mgmt.HPCfg{ + GroupId: &path_mgmt.HPGroupId{ + OwnerAS: h.Id.OwnerAS, + GroupId: h.Id.Suffix, + }, + Version: uint32(h.Version), + OwnerISD: h.Owner.I, + Writers: toIAInt(h.Writers), + Readers: toIAInt(h.Readers), + Registries: toIAInt(h.Registries), + } +} + +// FromMsg returns a HPCfg from the Cerializable representation +func FromMsg(m *path_mgmt.HPCfg) *Group { + return &Group{ + Id: GroupId{ + OwnerAS: m.GroupId.OwnerAS, + Suffix: m.GroupId.GroupId, + }, + Version: uint(m.Version), + Owner: addr.IA{ + I: m.OwnerISD, + A: m.GroupId.OwnerAS, + }, + Writers: toIA(m.Writers), + Readers: toIA(m.Readers), + Registries: toIA(m.Registries), + } +} + +func toIAInt(in []addr.IA) []addr.IAInt { + out := make([]addr.IAInt, 0, len(in)) + for _, i := range in { + out = append(out, i.IAInt()) + } + return out +} + +func toIA(in []addr.IAInt) []addr.IA { + out := make([]addr.IA, 0, len(in)) + for _, i := range in { + out = append(out, i.IA()) + } + return out +} diff --git a/go/lib/hiddenpath/group_test.go b/go/lib/hiddenpath/group_test.go new file mode 100644 index 0000000000..4277e37497 --- /dev/null +++ b/go/lib/hiddenpath/group_test.go @@ -0,0 +1,298 @@ +package hiddenpath + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/scionproto/scion/go/lib/addr" + "github.com/scionproto/scion/go/lib/ctrl/path_mgmt" + "github.com/scionproto/scion/go/lib/xtest" +) + +var testCfg = `{ + "GroupID": "ff00:0:110-69b5", + "Version": 1, + "Owner": "1-ff00:0:110", + "Writers": [ + "1-ff00:0:111", + "1-ff00:0:112" + ], + "Readers": [ + "1-ff00:0:113", + "1-ff00:0:114" + ], + "Registries": [ + "1-ff00:0:110", + "1-ff00:0:111", + "1-ff00:0:115" + ] +}` + +var testGroup = Group{ + Id: GroupId{ + OwnerAS: as110, + Suffix: 0x69b5, + }, + Version: 1, + Owner: ia110, + Writers: []addr.IA{ia111, ia112}, + Readers: []addr.IA{ia113, ia114}, + Registries: []addr.IA{ia110, ia111, ia115}, +} + +var ( + as110 = xtest.MustParseAS("ff00:0:110") + ia110 = xtest.MustParseIA("1-ff00:0:110") + ia111 = xtest.MustParseIA("1-ff00:0:111") + ia112 = xtest.MustParseIA("1-ff00:0:112") + ia113 = xtest.MustParseIA("1-ff00:0:113") + ia114 = xtest.MustParseIA("1-ff00:0:114") + ia115 = xtest.MustParseIA("1-ff00:0:115") +) + +func TestUnmarshalJSON(t *testing.T) { + tests := map[string]struct { + Modify func() string + ExpectedErrMsg string + ExpectedGroup func() Group + }{ + "valid": { + Modify: func() string { + return testCfg + }, + ExpectedErrMsg: "", + ExpectedGroup: func() Group { return testGroup }, + }, + "missing GroupId": { + Modify: func() string { + return strings.Replace(testCfg, `"GroupID": "ff00:0:110-69b5",`, "", 1) + }, + ExpectedErrMsg: `Missing GroupId`, + }, + "invalid GroupId format": { + Modify: func() string { + return strings.Replace(testCfg, "ff00:0:110-69b5", "invalid", 1) + }, + ExpectedErrMsg: `Invalid GroupId format GroupId="invalid"`, + }, + "invalid GroupId AS": { + Modify: func() string { + return strings.Replace(testCfg, "ff00:0:110", "invalid", 1) + }, + ExpectedErrMsg: `Unable to parse AS`, + }, + "invalid GroupId suffix": { + Modify: func() string { + return strings.Replace(testCfg, "69b5", "invalid", 1) + }, + ExpectedErrMsg: `Invalid GroupId suffix Suffix="invalid"`, + }, + "missing version": { + Modify: func() string { + return strings.Replace(testCfg, `"Version": 1,`, "", 1) + }, + ExpectedErrMsg: `Invalid version`, + }, + "invalid version": { + Modify: func() string { + return strings.Replace(testCfg, `"Version": 1,`, `"Version": 0,`, 1) + }, + ExpectedErrMsg: `Invalid version`, + }, + "missing Owner": { + Modify: func() string { + return strings.Replace(testCfg, `"Owner": "1-ff00:0:110",`, "", 1) + }, + ExpectedErrMsg: `Missing Owner`, + }, + "invalid Owner": { + Modify: func() string { + return strings.Replace(testCfg, "1-ff00:0:110", "invalid", 1) + }, + ExpectedErrMsg: `Invalid ISD-AS raw="invalid"`, + }, + "owner mismatch": { + Modify: func() string { + return strings.Replace(testCfg, "ff00:0:110", "ffaa:0:110", 1) + }, + ExpectedErrMsg: `Owner mismatch OwnerAS="ff00:0:110" GroupId.OwnerAS="ffaa:0:110"`, + }, + "missing Writers": { + Modify: func() string { + g := testGroup + g.Writers = nil + b, _ := json.Marshal(g) + return string(b) + }, + ExpectedErrMsg: `Writer section cannot be empty`, + }, + "empty Writers": { + Modify: func() string { + g := testGroup + g.Writers = []addr.IA{} + b, _ := json.Marshal(g) + return string(b) + }, + ExpectedErrMsg: `Writer section cannot be empty`, + }, + "invalid Writer": { + Modify: func() string { + return strings.Replace(testCfg, "1-ff00:0:111", "invalid", 1) + }, + ExpectedErrMsg: `Invalid ISD-AS raw="invalid"`, + }, + "missing Readers ok": { + Modify: func() string { + g := testGroup + g.Readers = nil + b, _ := json.Marshal(g) + return string(b) + }, + ExpectedGroup: func() Group { + g := testGroup + g.Readers = nil + return g + }, + }, + "empty Readers ok": { + Modify: func() string { + g := testGroup + g.Readers = []addr.IA{} + b, _ := json.Marshal(g) + return string(b) + }, + ExpectedGroup: func() Group { + g := testGroup + g.Readers = []addr.IA{} + return g + }, + }, + "invalid Reader": { + Modify: func() string { + return strings.Replace(testCfg, "1-ff00:0:114", "invalid", 1) + }, + ExpectedErrMsg: `Invalid ISD-AS raw="invalid"`, + }, + "missing Registries": { + Modify: func() string { + g := testGroup + g.Registries = nil + b, _ := json.Marshal(g) + return string(b) + }, + ExpectedErrMsg: `Registry section cannot be empty`, + }, + "empty Registries": { + Modify: func() string { + g := testGroup + g.Registries = []addr.IA{} + b, _ := json.Marshal(g) + return string(b) + }, + ExpectedErrMsg: `Registry section cannot be empty`, + }, + "invalid Registry": { + Modify: func() string { + return strings.Replace(testCfg, "1-ff00:0:115", "invalid", 1) + }, + ExpectedErrMsg: `Invalid ISD-AS raw="invalid"`, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var parsed Group + err := json.Unmarshal([]byte(test.Modify()), &parsed) + if test.ExpectedErrMsg == "" { + require.NoError(t, err) + require.Equal(t, test.ExpectedGroup(), parsed) + } else { + require.Error(t, err) + assert.Contains(t, err.Error(), test.ExpectedErrMsg) + } + }) + } +} + +func TestUnmarshalMarshal(t *testing.T) { + cfg := &Group{} + err := json.Unmarshal([]byte(testCfg), cfg) + require.NoError(t, err) + b, err := json.MarshalIndent(cfg, "", " ") + require.NoError(t, err) + assert.Equal(t, testCfg, string(b)) +} + +func TestToMsgFromMsg(t *testing.T) { + expected := &path_mgmt.HPCfg{ + GroupId: &path_mgmt.HPGroupId{ + OwnerAS: as110, + GroupId: 0x69b5, + }, + Version: 0x1, + OwnerISD: 0x1, + Writers: []addr.IAInt{ia111.IAInt(), ia112.IAInt()}, + Readers: []addr.IAInt{ia113.IAInt(), ia114.IAInt()}, + Registries: []addr.IAInt{ia110.IAInt(), ia111.IAInt(), ia115.IAInt()}, + } + cfg := &Group{} + err := json.Unmarshal([]byte(testCfg), cfg) + require.NoError(t, err) + msg := cfg.ToMsg() + assert.Equal(t, expected, msg) + cfg2 := FromMsg(msg) + assert.Equal(t, cfg, cfg2) +} + +func TestHas(t *testing.T) { + cfg := &Group{} + err := json.Unmarshal([]byte(testCfg), cfg) + require.NoError(t, err) + + tests := map[string]struct { + IA addr.IA + Func func(addr.IA) bool + Expected bool + }{ + "has writer": { + IA: cfg.Writers[0], + Func: cfg.HasWriter, + Expected: true, + }, + "not has writer": { + IA: cfg.Readers[0], + Func: cfg.HasWriter, + Expected: false, + }, + "has reader": { + IA: cfg.Readers[0], + Func: cfg.HasReader, + Expected: true, + }, + "not has reader": { + IA: cfg.Writers[0], + Func: cfg.HasReader, + Expected: false, + }, + "has registry": { + IA: cfg.Registries[0], + Func: cfg.HasRegistry, + Expected: true, + }, + "not has registry": { + IA: cfg.Writers[1], + Func: cfg.HasRegistry, + Expected: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert.Equal(t, test.Expected, test.Func(test.IA)) + }) + } +}