diff --git a/WORKSPACE b/WORKSPACE index 5d0d9817..5011b834 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -599,3 +599,15 @@ go_repository( commit = "c182affec369e30f25d3eb8cd8a478dee585ae7d", importpath = "github.com/matttproud/golang_protobuf_extensions", ) + +go_repository( + name = "com_github_golang_collections_go_datastructures", + commit = "59788d5eb2591d3497ffb8fafed2f16fe00e7775", + importpath = "github.com/golang-collections/go-datastructures", +) + +go_repository( + name = "com_github_go_test_deep", + commit = "cf67d735e69b4a4d50cdf571a92b0144786080f7", + importpath = "github.com/go-test/deep", +) diff --git a/bgpwtf/cccampix/irr/BUILD.bazel b/bgpwtf/cccampix/irr/BUILD.bazel new file mode 100644 index 00000000..64b8e322 --- /dev/null +++ b/bgpwtf/cccampix/irr/BUILD.bazel @@ -0,0 +1,22 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "go_default_library", + srcs = ["main.go"], + importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/irr", + visibility = ["//visibility:private"], + deps = [ + "//bgpwtf/cccampix/irr/provider:go_default_library", + "//bgpwtf/cccampix/proto:go_default_library", + "//go/mirko:go_default_library", + "@com_github_golang_glog//:go_default_library", + "@org_golang_google_grpc//codes:go_default_library", + "@org_golang_google_grpc//status:go_default_library", + ], +) + +go_binary( + name = "irr", + embed = [":go_default_library"], + visibility = ["//visibility:public"], +) diff --git a/bgpwtf/cccampix/irr/README.md b/bgpwtf/cccampix/irr/README.md new file mode 100644 index 00000000..de88d8b1 --- /dev/null +++ b/bgpwtf/cccampix/irr/README.md @@ -0,0 +1,69 @@ +irr +=== + +A proxy to access IRR RPSL data. It queries IANA for the responsible IRR, then the IRR directly. + +It currently support ARIN and RIPE. + +It currently supports querying for information about an aut-num, and returns the following attributes: + - remarks + - import + - export + +Example +------- + + $ grpcurl -format text -plaintext -d 'as: "26625"' 127.0.0.1:4220 ix.IRR.Query + source: SOURCE_ARIN + attributes: < + import: < + expressions: < + peering: "AS6083" + actions: "pref=10" + > + filter: "ANY" + > + > + attributes: < + import: < + expressions: < + peering: "AS12491" + actions: "pref=10" + > + filter: "ANY" + > + > + attributes: < + import: < + expressions: < + peering: "AS20459" + actions: "pref=10" + > + filter: "ANY" + > + > + attributes: < + export: < + expressions: < + peering: "AS6083" + > + filter: "AS26625" + > + > + attributes: < + export: < + expressions: < + peering: "AS12491" + > + filter: "AS26625" + > + > + attributes: < + export: < + expressions: < + peering: "AS20459" + > + filter: "AS26625" + > + > + diff --git a/bgpwtf/cccampix/irr/main.go b/bgpwtf/cccampix/irr/main.go new file mode 100644 index 00000000..1492e6cc --- /dev/null +++ b/bgpwtf/cccampix/irr/main.go @@ -0,0 +1,76 @@ +package main + +import ( + "context" + "flag" + "strconv" + "strings" + + "github.com/golang/glog" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "code.hackerspace.pl/hscloud/bgpwtf/cccampix/irr/provider" + pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto" + "code.hackerspace.pl/hscloud/go/mirko" +) + +type service struct { + iana *provider.IANA + providers map[provider.IRR]provider.Provider +} + +func main() { + flag.Parse() + mi := mirko.New() + + if err := mi.Listen(); err != nil { + glog.Exitf("Listen failed: %v", err) + } + + s := &service{ + iana: provider.NewIANA(), + providers: map[provider.IRR]provider.Provider{ + provider.IRR_RIPE: provider.NewRIPE(), + provider.IRR_ARIN: provider.NewARIN(), + }, + } + pb.RegisterIRRServer(mi.GRPC(), s) + + if err := mi.Serve(); err != nil { + glog.Exitf("Serve failed: %v", err) + } + + <-mi.Done() +} + +// Query returns parsed RPSL data for a given aut-num objects in any of the +// supported IRRs. +func (s *service) Query(ctx context.Context, req *pb.IRRQueryRequest) (*pb.IRRQueryResponse, error) { + if req.As == "" { + return nil, status.Error(codes.InvalidArgument, "as must be given") + } + + req.As = strings.ToLower(req.As) + if strings.HasPrefix(req.As, "as") { + req.As = req.As[2:] + } + + asn, err := strconv.ParseUint(req.As, 10, 64) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "as is invalid") + } + + irr, err := s.iana.Who(ctx, asn) + if err != nil { + return nil, status.Errorf(codes.Unavailable, "could not find AS block delegation from IANA: %v", err) + } + + prov, ok := s.providers[irr] + if !ok { + return nil, status.Errorf(codes.NotFound, "AS belongs to unhandled IRR %s", irr.String()) + } + + res, err := prov.Query(ctx, asn) + return res, err +} diff --git a/bgpwtf/cccampix/irr/provider/BUILD.bazel b/bgpwtf/cccampix/irr/provider/BUILD.bazel new file mode 100644 index 00000000..f39744eb --- /dev/null +++ b/bgpwtf/cccampix/irr/provider/BUILD.bazel @@ -0,0 +1,30 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "arin.go", + "iana.go", + "provider.go", + "ripe.go", + "rpsl.go", + ], + importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/irr/provider", + visibility = ["//visibility:public"], + deps = [ + "//bgpwtf/cccampix/irr/whois:go_default_library", + "//bgpwtf/cccampix/proto:go_default_library", + "@com_github_golang_collections_go_datastructures//augmentedtree:go_default_library", + "@com_github_golang_glog//:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["rpsl_test.go"], + embed = [":go_default_library"], + deps = [ + "//bgpwtf/cccampix/proto:go_default_library", + "@com_github_go_test_deep//:go_default_library", + ], +) diff --git a/bgpwtf/cccampix/irr/provider/arin.go b/bgpwtf/cccampix/irr/provider/arin.go new file mode 100644 index 00000000..1781198b --- /dev/null +++ b/bgpwtf/cccampix/irr/provider/arin.go @@ -0,0 +1,78 @@ +package provider + +// Support for the ARIN IRR. +// ARIN is special. We have to query them via whois. It's also not the same +// whois as you usually see. And also not many autnums (even big players like +// AS15169 and AS112) have IRR entries at all. + +import ( + "context" + "fmt" + "strings" + + "code.hackerspace.pl/hscloud/bgpwtf/cccampix/irr/whois" + pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto" +) + +const ARINWhois = "rr.arin.net:43" + +type arin struct { +} + +func NewARIN() Provider { + return &arin{} +} + +func (r *arin) Query(ctx context.Context, asn uint64) (*pb.IRRQueryResponse, error) { + data, err := whois.Query(ctx, ARINWhois, fmt.Sprintf("AS%d", asn)) + if err != nil { + return nil, fmt.Errorf("could not contact ARIN IRR: %v", err) + } + + lines := strings.Split(data, "\n") + + // Convert possibly 'continued' RPSL entries into single-line entries. + // eg. + // import: from AS6083 + // action pref=10; + // accept ANY + // into + // 'import', 'from AS6083, action pref=10; accept ANY' + + attrs := []rpslRawAttribute{} + for _, line := range lines { + if strings.HasPrefix(strings.TrimSpace(line), "%") { + // Comment + continue + } + if strings.TrimSpace(line) == "" { + // Empty line + continue + } + + if strings.HasPrefix(line, " ") { + // Continuation + if len(attrs) < 1 { + return nil, fmt.Errorf("unparseable IRR, continuation with no previous atribute name: %q", line) + } + + attrs[len(attrs)-1].value += " " + strings.TrimSpace(line) + } else { + parts := strings.SplitN(line, ":", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("unparseable IRR, line with no attribute key: %q", line) + } + name := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + attrs = append(attrs, rpslRawAttribute{ + name: name, + value: value, + }) + } + } + + return &pb.IRRQueryResponse{ + Source: pb.IRRQueryResponse_SOURCE_ARIN, + Attributes: parseAttributes(attrs), + }, nil +} diff --git a/bgpwtf/cccampix/irr/provider/iana.go b/bgpwtf/cccampix/irr/provider/iana.go new file mode 100644 index 00000000..8f085d26 --- /dev/null +++ b/bgpwtf/cccampix/irr/provider/iana.go @@ -0,0 +1,163 @@ +package provider + +// IANA is not a full provider - we use it to determine the relevant IRR for a given aut-num. + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/golang-collections/go-datastructures/augmentedtree" + "github.com/golang/glog" + + "code.hackerspace.pl/hscloud/bgpwtf/cccampix/irr/whois" +) + +// Possible IRRs that AS blocks can be delegated to. +type IRR int + +const ( + IRR_UNKNOWN IRR = iota + IRR_ARIN + IRR_RIPE +) + +func (i IRR) String() string { + return []string{ + "UNKNOWN", + "ARIN", + "RIPE", + }[i] +} + +// IANA access service. +type IANA struct { + // Interval tree of AS block delegation to IRRs. We use this to not + // keep hitting the IANA whois unnecessariy. + cache augmentedtree.Tree + // The tree library needs intervals to have a unique ID. We use a counter + // for this effect. + id uint64 +} + +func NewIANA() *IANA { + return &IANA{ + cache: augmentedtree.New(1), + } +} + +func (i *IANA) nextID() uint64 { + res := i.id + i.id += 1 + return res +} + +// delegation implements the Interval interface for the interval tree. +type delegation struct { + to IRR + id uint64 + low int64 + high int64 +} + +func (d *delegation) LowAtDimension(n uint64) int64 { + if n != 1 { + panic(fmt.Sprintf("dimension too high (%d)", n)) + } + return d.low +} + +func (d *delegation) HighAtDimension(n uint64) int64 { + if n != 1 { + panic(fmt.Sprintf("dimension too high (%d)", n)) + } + return d.high +} + +func (d *delegation) OverlapsAtDimension(i augmentedtree.Interval, n uint64) bool { + if n != 1 { + return false + } + + if i.LowAtDimension(1) <= d.HighAtDimension(1) && i.HighAtDimension(1) >= d.LowAtDimension(1) { + return true + } + + return false +} + +func (d *delegation) ID() uint64 { + return d.id +} + +// Who returns the responsible IRR (or UNKNOWN) for a given AS. +func (i *IANA) Who(ctx context.Context, asn uint64) (IRR, error) { + q := &delegation{ + id: i.nextID(), + low: int64(asn), + high: int64(asn), + } + + res := i.cache.Query(q) + if len(res) > 0 { + return res[0].(*delegation).to, nil + } + + // No cache entry, query whois. + glog.Infof("Cache miss for AS%d", asn) + data, err := whois.Query(ctx, "whois.iana.org:43", fmt.Sprintf("AS%d", asn)) + if err != nil { + return IRR_UNKNOWN, err + } + + // We not only find the responsible IRR, but also the delegation bounds + // to feed the interval tree. + lines := strings.Split(data, "\n") + var lower int64 + var upper int64 + var irr IRR + for _, line := range lines { + parts := strings.Fields(line) + if len(parts) == 2 && parts[0] == "as-block:" { + bounds := strings.Split(parts[1], "-") + if len(bounds) != 2 { + return IRR_UNKNOWN, fmt.Errorf("unparseable as-block: %v", parts[1]) + } + lower, err = strconv.ParseInt(bounds[0], 10, 64) + if err != nil { + return IRR_UNKNOWN, fmt.Errorf("unparseable as-block: %v", parts[1]) + } + upper, err = strconv.ParseInt(bounds[1], 10, 64) + if err != nil { + return IRR_UNKNOWN, fmt.Errorf("unparseable as-block: %v", parts[1]) + } + } + if len(parts) > 2 && parts[0] == "organisation:" { + switch { + case strings.Contains(line, "RIPE NCC"): + irr = IRR_RIPE + case strings.Contains(line, "ARIN"): + irr = IRR_ARIN + } + } + } + + if lower == 0 || upper == 0 { + return IRR_UNKNOWN, nil + } + + glog.Infof("Saving %d-%d AS blocks delegation (%s) to cache", lower, upper, irr.String()) + + // Insert into tree. + q = &delegation{ + id: i.nextID(), + to: irr, + low: lower, + high: upper + 1, + } + + i.cache.Add(q) + + return irr, nil +} diff --git a/bgpwtf/cccampix/irr/provider/provider.go b/bgpwtf/cccampix/irr/provider/provider.go new file mode 100644 index 00000000..4e2921b5 --- /dev/null +++ b/bgpwtf/cccampix/irr/provider/provider.go @@ -0,0 +1,13 @@ +package provider + +import ( + "context" + + pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto" +) + +// Provider is the interface exposed to the service by IRR proxies. +type Provider interface { + // Return a proto response for a given AS. + Query(ctx context.Context, as uint64) (*pb.IRRQueryResponse, error) +} diff --git a/bgpwtf/cccampix/irr/provider/ripe.go b/bgpwtf/cccampix/irr/provider/ripe.go new file mode 100644 index 00000000..5b09b9f5 --- /dev/null +++ b/bgpwtf/cccampix/irr/provider/ripe.go @@ -0,0 +1,81 @@ +package provider + +// Support for the RIPE IRR. +// We use the RIPE REST DB API. + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto" +) + +type ripeResponse struct { + Objects struct { + Object []ripeObject `json:"object"` + } `json:"objects"` +} + +type ripeObject struct { + Type string `json:"type"` + Attributes struct { + Attribute []ripeAttribute `json:"attribute"` + } `json:"attributes"` +} + +type ripeAttribute struct { + Name string `json:"name"` + Value string `json:"value"` +} + +type ripe struct { +} + +func NewRIPE() Provider { + return &ripe{} +} + +func (r *ripe) Query(ctx context.Context, as uint64) (*pb.IRRQueryResponse, error) { + req, err := http.NewRequest("GET", fmt.Sprintf("http://rest.db.ripe.net/ripe/aut-num/AS%d.json", as), nil) + if err != nil { + return nil, err + } + + req = req.WithContext(ctx) + client := http.DefaultClient + + res, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("could not run GET to RIPE: %v", err) + } + defer res.Body.Close() + bytes, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("could not read response from RIPE: %v", err) + } + + data := ripeResponse{} + err = json.Unmarshal(bytes, &data) + if err != nil { + return nil, fmt.Errorf("could not decode response from RIPE: %v", err) + } + + if len(data.Objects.Object) != 1 { + return nil, fmt.Errorf("could not retriev aut-num from RIPE") + } + + attributes := make([]rpslRawAttribute, len(data.Objects.Object[0].Attributes.Attribute)) + + for i, attr := range data.Objects.Object[0].Attributes.Attribute { + attributes[i].name = attr.Name + attributes[i].value = attr.Value + } + + return &pb.IRRQueryResponse{ + Source: pb.IRRQueryResponse_SOURCE_RIPE, + Attributes: parseAttributes(attributes), + }, nil +} diff --git a/bgpwtf/cccampix/irr/provider/rpsl.go b/bgpwtf/cccampix/irr/provider/rpsl.go new file mode 100644 index 00000000..68a1f51b --- /dev/null +++ b/bgpwtf/cccampix/irr/provider/rpsl.go @@ -0,0 +1,146 @@ +package provider + +import ( + "strings" + + pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto" +) + +// A raw, unparsed RPSL name/value pair. +type rpslRawAttribute struct { + name string + value string +} + +// parseAttributes converts raw RPSL attributes into parsed, structured protos. +func parseAttributes(attrs []rpslRawAttribute) []*pb.IRRAttribute { + res := []*pb.IRRAttribute{} + + for _, attr := range attrs { + switch attr.name { + case "remarks": + res = append(res, &pb.IRRAttribute{ + Value: &pb.IRRAttribute_Remarks{attr.value}, + }) + case "import": + ie := parseImportExport(attr.value) + if ie != nil { + res = append(res, &pb.IRRAttribute{ + Value: &pb.IRRAttribute_Import{ie}, + }) + } + case "export": + ie := parseImportExport(attr.value) + if ie != nil { + res = append(res, &pb.IRRAttribute{ + Value: &pb.IRRAttribute_Export{ie}, + }) + } + } + } + + return res +} + +// parseImportExport tries to parse the RPSL subset for import/export +// attributes. +// It's a hand-written single-pass parser which makes it not very good. +// See rpsl_test.go for examples. +func parseImportExport(expression string) *pb.IRRAttribute_ImportExport { + popToken := func(s string) (string, string) { + s = strings.TrimSpace(s) + fields := strings.Fields(s) + if len(fields) < 2 { + return "", s + } + return fields[0], strings.TrimSpace(s[len(fields[0]):]) + } + + res := &pb.IRRAttribute_ImportExport{} + + expr := expression + var t string + t, expr = popToken(expr) + + for { + switch t { + case "": + break + case "protocol": + t, expr = popToken(expr) + res.ProtocolFrom = t + t, expr = popToken(expr) + continue + case "into": + t, expr = popToken(expr) + res.ProtocolInto = t + t, expr = popToken(expr) + continue + case "from": + fallthrough + case "to": + t, expr = popToken(expr) + peering := t + actions := []string{} + router_us := "" + router_them := "" + + t2, expr2 := popToken(expr) + for { + switch t2 { + case "at": + t2, expr2 = popToken(expr2) + router_us = t2 + t2, expr2 = popToken(expr2) + continue + case "action": + parts := strings.SplitN(expr2, ";", 2) + if len(parts) != 2 { + // malformed action, no ';' found - bail + return nil + } + actions = append(actions, parts[0]) + expr2 = strings.TrimSpace(parts[1]) + t2, expr2 = popToken(expr2) + continue + case "": + fallthrough + case "accept": + fallthrough + case "announce": + t = t2 + expr = expr2 + break + default: + if router_them == "" { + router_them = t2 + t2, expr2 = popToken(expr2) + continue + } + t = t2 + expr = expr2 + break + } + break + } + res.Expressions = append(res.Expressions, &pb.IRRAttribute_ImportExport_Expression{ + Peering: peering, + RouterUs: router_us, + RouterThem: router_them, + Actions: actions, + }) + continue + case "accept": + fallthrough + case "announce": + res.Filter = expr + t = "" + continue + default: + return nil + } + break + } + + return res +} diff --git a/bgpwtf/cccampix/irr/provider/rpsl_test.go b/bgpwtf/cccampix/irr/provider/rpsl_test.go new file mode 100644 index 00000000..2c0fe577 --- /dev/null +++ b/bgpwtf/cccampix/irr/provider/rpsl_test.go @@ -0,0 +1,64 @@ +package provider + +import ( + "testing" + + pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto" + "github.com/go-test/deep" +) + +func TestParseImportExport(t *testing.T) { + tests := []struct { + ut string + want *pb.IRRAttribute_ImportExport + }{ + { + ut: ` + from AS10674 209.251.128.177 at 216.155.103.20 action pref = 1; + accept ANY AND NOT AS-ACCELERATION-CUST + `, + want: &pb.IRRAttribute_ImportExport{ + Expressions: []*pb.IRRAttribute_ImportExport_Expression{ + { + Peering: "AS10674", + RouterUs: "216.155.103.20", + RouterThem: "209.251.128.177", + Actions: []string{"pref = 1"}, + }, + }, + Filter: "ANY AND NOT AS-ACCELERATION-CUST", + }, + }, + { + ut: ` + to AS201054 94.246.185.174 at 94.246.185.175 announce AS-BGPWTF + `, + want: &pb.IRRAttribute_ImportExport{ + Expressions: []*pb.IRRAttribute_ImportExport_Expression{ + { + Peering: "AS201054", + RouterUs: "94.246.185.175", + RouterThem: "94.246.185.174", + Actions: []string{}, + }, + }, + Filter: "AS-BGPWTF", + }, + }, + { + // Invalid - unterminated action. + ut: ` + to AS201054 94.246.185.174 at 94.246.185.175 action foo = bar + accept ANY AND NOT AS-ACCELERATION-CUST + `, + want: nil, + }, + } + + for _, test := range tests { + res := parseImportExport(test.ut) + if diff := deep.Equal(test.want, res); diff != nil { + t.Error(diff) + } + } +} diff --git a/bgpwtf/cccampix/irr/whois/BUILD.bazel b/bgpwtf/cccampix/irr/whois/BUILD.bazel new file mode 100644 index 00000000..d30237da --- /dev/null +++ b/bgpwtf/cccampix/irr/whois/BUILD.bazel @@ -0,0 +1,8 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["whois.go"], + importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/irr/whois", + visibility = ["//visibility:public"], +) diff --git a/bgpwtf/cccampix/irr/whois/whois.go b/bgpwtf/cccampix/irr/whois/whois.go new file mode 100644 index 00000000..0e313b18 --- /dev/null +++ b/bgpwtf/cccampix/irr/whois/whois.go @@ -0,0 +1,32 @@ +package whois + +// Support for the WHOIS protocol. + +import ( + "context" + "fmt" + "io/ioutil" + "net" + "strings" +) + +// Query returns a semi-raw response from a WHOIS server for a given query. +// We only convert \r\n to \n, and then do no other transformation to the data. +func Query(ctx context.Context, server string, query string) (string, error) { + var d net.Dialer + conn, err := d.DialContext(ctx, "tcp", server) + if err != nil { + return "", fmt.Errorf("while dialing %q: %v", server, err) + } + + defer conn.Close() + + fmt.Fprintf(conn, "%s\r\n", query) + + data, err := ioutil.ReadAll(conn) + if err != nil { + return "", fmt.Errorf("while receiving data from %q: %v", server, err) + } + + return strings.ReplaceAll(string(data), "\r", ""), nil +} diff --git a/bgpwtf/cccampix/proto/ix.proto b/bgpwtf/cccampix/proto/ix.proto index c938386d..01ddc4fb 100644 --- a/bgpwtf/cccampix/proto/ix.proto +++ b/bgpwtf/cccampix/proto/ix.proto @@ -39,3 +39,45 @@ service PeeringDBProxy { // GetIXMembers returns information about membership of a given PeeringDB IX. rpc GetIXMembers(GetIXMembersRequest) returns (GetIXMembersResponse); } + +message IRRQueryRequest { + // AS to query for. This needs be the AS number of the AS, possibly + // prefixed with 'as'/'AS'. + string as = 1; +} + +message IRRAttribute { + message ImportExport { + message Expression { + string peering = 1; + string router_us = 2; + string router_them = 3; + repeated string actions = 4; + } + string protocol_from = 1; + string protocol_into = 2; + repeated Expression expressions = 3; + string filter = 4; + } + + oneof value { + string remarks = 1; + ImportExport import = 2; + ImportExport export = 3; + } +} + +message IRRQueryResponse { + enum Source { + SOURCE_INVALID = 0; + SOURCE_RIPE = 1; + SOURCE_ARIN = 2; + } + Source source = 1; + repeated IRRAttribute attributes = 2; +} + +service IRR { + // Query returns parsed RPSL data from supported IRRs for a given aut-num. + rpc Query(IRRQueryRequest) returns (IRRQueryResponse); +}