summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml27
-rw-r--r--cmd/relay/main.go36
-rw-r--r--go.mod21
-rw-r--r--go.sum50
-rw-r--r--relay/proto/Makefile6
-rw-r--r--relay/proto/relay.pb.go354
-rw-r--r--relay/proto/relay.proto31
-rw-r--r--relay/proto/relay_grpc.pb.go177
-rw-r--r--relay/relay.go79
-rw-r--r--relay/relay_test.go86
-rw-r--r--relay/session.go23
11 files changed, 890 insertions, 0 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..9a479fc
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,27 @@
+name: Go CI
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version: '1.23'
+
+ - name: Test
+ run: go test ./... -v -race -coverprofile=coverage.txt
+
+ - name: Upload coverage reports to Codecov
+ uses: codecov/codecov-action@v5
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
diff --git a/cmd/relay/main.go b/cmd/relay/main.go
new file mode 100644
index 0000000..150da8a
--- /dev/null
+++ b/cmd/relay/main.go
@@ -0,0 +1,36 @@
+package main
+
+import (
+ "net"
+ "os"
+
+ "github.com/boozec/rahanna/relay"
+ pb "github.com/boozec/rahanna/relay/proto"
+ "go.uber.org/zap"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/reflection"
+)
+
+func main() {
+ logger, _ := zap.NewProduction()
+ defer logger.Sync()
+ lis, err := net.Listen("tcp", ":50051")
+
+ if err != nil {
+ logger.Sugar().Errorln("Failed to listen", "err", err)
+ os.Exit(1)
+ }
+
+ s := grpc.NewServer()
+ server := &relay.Server{}
+ pb.RegisterRelayServer(s, server)
+
+ reflection.Register(s)
+
+ logger.Sugar().Infoln("Server listening", "address", lis.Addr())
+
+ if err := s.Serve(lis); err != nil {
+ logger.Sugar().Errorln("Failed to serve", "err", err)
+ os.Exit(1)
+ }
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..c50007c
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,21 @@
+module github.com/boozec/rahanna
+
+go 1.24.0
+
+require (
+ github.com/stretchr/testify v1.10.0
+ go.uber.org/zap v1.27.0
+ google.golang.org/grpc v1.71.0
+ google.golang.org/protobuf v1.36.4
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ go.uber.org/multierr v1.10.0 // indirect
+ golang.org/x/net v0.34.0 // indirect
+ golang.org/x/sys v0.29.0 // indirect
+ golang.org/x/text v0.21.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..4491c73
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,50 @@
+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/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
+go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
+go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
+go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
+go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
+go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
+go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
+go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
+go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
+go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
+go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
+golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
+golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
+golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
+google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
+google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
+google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/relay/proto/Makefile b/relay/proto/Makefile
new file mode 100644
index 0000000..fa95be4
--- /dev/null
+++ b/relay/proto/Makefile
@@ -0,0 +1,6 @@
+protoc:
+ protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative relay.proto
+
+clean:
+ rm -rf relay.pb.go
+ rm -rf relay_grpc.pb.go
diff --git a/relay/proto/relay.pb.go b/relay/proto/relay.pb.go
new file mode 100644
index 0000000..2f88408
--- /dev/null
+++ b/relay/proto/relay.pb.go
@@ -0,0 +1,354 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.28.1
+// protoc v3.21.12
+// source: relay.proto
+
+package proto
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type RelayRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
+}
+
+func (x *RelayRequest) Reset() {
+ *x = RelayRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_relay_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *RelayRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RelayRequest) ProtoMessage() {}
+
+func (x *RelayRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_relay_proto_msgTypes[0]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RelayRequest.ProtoReflect.Descriptor instead.
+func (*RelayRequest) Descriptor() ([]byte, []int) {
+ return file_relay_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *RelayRequest) GetIp() string {
+ if x != nil {
+ return x.Ip
+ }
+ return ""
+}
+
+type LookupRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+}
+
+func (x *LookupRequest) Reset() {
+ *x = LookupRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_relay_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *LookupRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*LookupRequest) ProtoMessage() {}
+
+func (x *LookupRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_relay_proto_msgTypes[1]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use LookupRequest.ProtoReflect.Descriptor instead.
+func (*LookupRequest) Descriptor() ([]byte, []int) {
+ return file_relay_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *LookupRequest) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+type RelayResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+ Ip string `protobuf:"bytes,2,opt,name=ip,proto3" json:"ip,omitempty"`
+}
+
+func (x *RelayResponse) Reset() {
+ *x = RelayResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_relay_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *RelayResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RelayResponse) ProtoMessage() {}
+
+func (x *RelayResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_relay_proto_msgTypes[2]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RelayResponse.ProtoReflect.Descriptor instead.
+func (*RelayResponse) Descriptor() ([]byte, []int) {
+ return file_relay_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *RelayResponse) GetName() string {
+ if x != nil {
+ return x.Name
+ }
+ return ""
+}
+
+func (x *RelayResponse) GetIp() string {
+ if x != nil {
+ return x.Ip
+ }
+ return ""
+}
+
+type CloseResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Status bool `protobuf:"varint,1,opt,name=status,proto3" json:"status,omitempty"`
+}
+
+func (x *CloseResponse) Reset() {
+ *x = CloseResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_relay_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CloseResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CloseResponse) ProtoMessage() {}
+
+func (x *CloseResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_relay_proto_msgTypes[3]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CloseResponse.ProtoReflect.Descriptor instead.
+func (*CloseResponse) Descriptor() ([]byte, []int) {
+ return file_relay_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *CloseResponse) GetStatus() bool {
+ if x != nil {
+ return x.Status
+ }
+ return false
+}
+
+var File_relay_proto protoreflect.FileDescriptor
+
+var file_relay_proto_rawDesc = []byte{
+ 0x0a, 0x0b, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x1e, 0x0a,
+ 0x0c, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a,
+ 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x22, 0x23, 0x0a,
+ 0x0d, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12,
+ 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
+ 0x6d, 0x65, 0x22, 0x33, 0x0a, 0x0d, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x02, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x22, 0x27, 0x0a, 0x0d, 0x43, 0x6c, 0x6f, 0x73, 0x65,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74,
+ 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
+ 0x32, 0x93, 0x01, 0x0a, 0x05, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x12, 0x2f, 0x0a, 0x0c, 0x52, 0x65,
+ 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x0d, 0x2e, 0x52, 0x65, 0x6c,
+ 0x61, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x52, 0x65, 0x6c, 0x61,
+ 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2a, 0x0a, 0x06, 0x4c,
+ 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x12, 0x0e, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x79, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x2d, 0x0a, 0x09, 0x43, 0x6c, 0x6f, 0x73, 0x65,
+ 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x2e, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0e, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x27, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
+ 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x6f, 0x6f, 0x7a, 0x65, 0x63, 0x2f, 0x72, 0x61, 0x68, 0x61,
+ 0x6e, 0x6e, 0x61, 0x2f, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
+ 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+ file_relay_proto_rawDescOnce sync.Once
+ file_relay_proto_rawDescData = file_relay_proto_rawDesc
+)
+
+func file_relay_proto_rawDescGZIP() []byte {
+ file_relay_proto_rawDescOnce.Do(func() {
+ file_relay_proto_rawDescData = protoimpl.X.CompressGZIP(file_relay_proto_rawDescData)
+ })
+ return file_relay_proto_rawDescData
+}
+
+var file_relay_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_relay_proto_goTypes = []interface{}{
+ (*RelayRequest)(nil), // 0: RelayRequest
+ (*LookupRequest)(nil), // 1: LookupRequest
+ (*RelayResponse)(nil), // 2: RelayResponse
+ (*CloseResponse)(nil), // 3: CloseResponse
+}
+var file_relay_proto_depIdxs = []int32{
+ 0, // 0: Relay.RegisterName:input_type -> RelayRequest
+ 1, // 1: Relay.Lookup:input_type -> LookupRequest
+ 1, // 2: Relay.CloseName:input_type -> LookupRequest
+ 2, // 3: Relay.RegisterName:output_type -> RelayResponse
+ 2, // 4: Relay.Lookup:output_type -> RelayResponse
+ 3, // 5: Relay.CloseName:output_type -> CloseResponse
+ 3, // [3:6] is the sub-list for method output_type
+ 0, // [0:3] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_relay_proto_init() }
+func file_relay_proto_init() {
+ if File_relay_proto != nil {
+ return
+ }
+ if !protoimpl.UnsafeEnabled {
+ file_relay_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*RelayRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_relay_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*LookupRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_relay_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*RelayResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_relay_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CloseResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: file_relay_proto_rawDesc,
+ NumEnums: 0,
+ NumMessages: 4,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_relay_proto_goTypes,
+ DependencyIndexes: file_relay_proto_depIdxs,
+ MessageInfos: file_relay_proto_msgTypes,
+ }.Build()
+ File_relay_proto = out.File
+ file_relay_proto_rawDesc = nil
+ file_relay_proto_goTypes = nil
+ file_relay_proto_depIdxs = nil
+}
diff --git a/relay/proto/relay.proto b/relay/proto/relay.proto
new file mode 100644
index 0000000..cbad184
--- /dev/null
+++ b/relay/proto/relay.proto
@@ -0,0 +1,31 @@
+syntax = "proto3";
+
+option go_package = "github.com/boozec/rahanna/relay/proto";
+
+service Relay
+{
+ rpc RegisterName(RelayRequest) returns (RelayResponse) {}
+ rpc Lookup(LookupRequest) returns (RelayResponse) {}
+ rpc CloseName(LookupRequest) returns (CloseResponse) {}
+}
+
+message RelayRequest
+{
+ string ip = 1;
+}
+
+message LookupRequest
+{
+ string name = 1;
+}
+
+message RelayResponse
+{
+ string name = 1;
+ string ip = 2;
+}
+
+message CloseResponse
+{
+ bool status = 1;
+}
diff --git a/relay/proto/relay_grpc.pb.go b/relay/proto/relay_grpc.pb.go
new file mode 100644
index 0000000..a102e96
--- /dev/null
+++ b/relay/proto/relay_grpc.pb.go
@@ -0,0 +1,177 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.2.0
+// - protoc v3.21.12
+// source: relay.proto
+
+package proto
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+// RelayClient is the client API for Relay service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type RelayClient interface {
+ RegisterName(ctx context.Context, in *RelayRequest, opts ...grpc.CallOption) (*RelayResponse, error)
+ Lookup(ctx context.Context, in *LookupRequest, opts ...grpc.CallOption) (*RelayResponse, error)
+ CloseName(ctx context.Context, in *LookupRequest, opts ...grpc.CallOption) (*CloseResponse, error)
+}
+
+type relayClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewRelayClient(cc grpc.ClientConnInterface) RelayClient {
+ return &relayClient{cc}
+}
+
+func (c *relayClient) RegisterName(ctx context.Context, in *RelayRequest, opts ...grpc.CallOption) (*RelayResponse, error) {
+ out := new(RelayResponse)
+ err := c.cc.Invoke(ctx, "/Relay/RegisterName", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *relayClient) Lookup(ctx context.Context, in *LookupRequest, opts ...grpc.CallOption) (*RelayResponse, error) {
+ out := new(RelayResponse)
+ err := c.cc.Invoke(ctx, "/Relay/Lookup", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *relayClient) CloseName(ctx context.Context, in *LookupRequest, opts ...grpc.CallOption) (*CloseResponse, error) {
+ out := new(CloseResponse)
+ err := c.cc.Invoke(ctx, "/Relay/CloseName", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// RelayServer is the server API for Relay service.
+// All implementations must embed UnimplementedRelayServer
+// for forward compatibility
+type RelayServer interface {
+ RegisterName(context.Context, *RelayRequest) (*RelayResponse, error)
+ Lookup(context.Context, *LookupRequest) (*RelayResponse, error)
+ CloseName(context.Context, *LookupRequest) (*CloseResponse, error)
+ mustEmbedUnimplementedRelayServer()
+}
+
+// UnimplementedRelayServer must be embedded to have forward compatible implementations.
+type UnimplementedRelayServer struct {
+}
+
+func (UnimplementedRelayServer) RegisterName(context.Context, *RelayRequest) (*RelayResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method RegisterName not implemented")
+}
+func (UnimplementedRelayServer) Lookup(context.Context, *LookupRequest) (*RelayResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method Lookup not implemented")
+}
+func (UnimplementedRelayServer) CloseName(context.Context, *LookupRequest) (*CloseResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method CloseName not implemented")
+}
+func (UnimplementedRelayServer) mustEmbedUnimplementedRelayServer() {}
+
+// UnsafeRelayServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to RelayServer will
+// result in compilation errors.
+type UnsafeRelayServer interface {
+ mustEmbedUnimplementedRelayServer()
+}
+
+func RegisterRelayServer(s grpc.ServiceRegistrar, srv RelayServer) {
+ s.RegisterService(&Relay_ServiceDesc, srv)
+}
+
+func _Relay_RegisterName_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(RelayRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(RelayServer).RegisterName(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/Relay/RegisterName",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(RelayServer).RegisterName(ctx, req.(*RelayRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Relay_Lookup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(LookupRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(RelayServer).Lookup(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/Relay/Lookup",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(RelayServer).Lookup(ctx, req.(*LookupRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _Relay_CloseName_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(LookupRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(RelayServer).CloseName(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/Relay/CloseName",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(RelayServer).CloseName(ctx, req.(*LookupRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// Relay_ServiceDesc is the grpc.ServiceDesc for Relay service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var Relay_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "Relay",
+ HandlerType: (*RelayServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "RegisterName",
+ Handler: _Relay_RegisterName_Handler,
+ },
+ {
+ MethodName: "Lookup",
+ Handler: _Relay_Lookup_Handler,
+ },
+ {
+ MethodName: "CloseName",
+ Handler: _Relay_CloseName_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "relay.proto",
+}
diff --git a/relay/relay.go b/relay/relay.go
new file mode 100644
index 0000000..b52a923
--- /dev/null
+++ b/relay/relay.go
@@ -0,0 +1,79 @@
+package relay
+
+import (
+ "context"
+ "fmt"
+ "sync"
+
+ pb "github.com/boozec/rahanna/relay/proto"
+)
+
+type Server struct {
+ pb.UnimplementedRelayServer
+}
+
+type name string
+
+// TODO: use pair of ips and ports
+type ips struct {
+ ip0 string
+ ip1 string
+}
+
+var mu sync.Mutex
+
+// Map each name to a pair of IPs
+var table = make(map[name]ips)
+
+func (s *Server) RegisterName(ctx context.Context, in *pb.RelayRequest) (*pb.RelayResponse, error) {
+ mu.Lock()
+ defer mu.Unlock()
+
+ if in.Ip == "" {
+ return nil, fmt.Errorf("IP address cannot be empty")
+ }
+
+ sessionName := newSession()
+ for {
+ if _, ok := table[name(sessionName)]; !ok {
+ break
+ }
+ sessionName = newSession()
+ }
+
+ table[name(sessionName)] = ips{ip0: in.Ip, ip1: ""}
+ return &pb.RelayResponse{Name: sessionName, Ip: in.Ip}, nil
+}
+
+func (s *Server) Lookup(ctx context.Context, in *pb.LookupRequest) (*pb.RelayResponse, error) {
+ mu.Lock()
+ defer mu.Unlock()
+
+ if in.Name == "" {
+ return nil, fmt.Errorf("name cannot be empty")
+ }
+
+ entry, ok := table[name(in.Name)]
+ if !ok {
+ return nil, fmt.Errorf("name not found")
+ }
+
+ return &pb.RelayResponse{Name: in.Name, Ip: entry.ip0}, nil
+}
+
+func (s *Server) CloseName(ctx context.Context, in *pb.LookupRequest) (*pb.CloseResponse, error) {
+ mu.Lock()
+ defer mu.Unlock()
+
+ if in.Name == "" {
+ return nil, fmt.Errorf("name cannot be empty")
+ }
+
+ _, ok := table[name(in.Name)]
+ if !ok {
+ return &pb.CloseResponse{Status: false}, fmt.Errorf("name not found")
+ }
+
+ delete(table, name(in.Name))
+ return &pb.CloseResponse{Status: true}, nil
+}
diff --git a/relay/relay_test.go b/relay/relay_test.go
new file mode 100644
index 0000000..25fcc35
--- /dev/null
+++ b/relay/relay_test.go
@@ -0,0 +1,86 @@
+package relay
+
+import (
+ "context"
+ "testing"
+
+ pb "github.com/boozec/rahanna/relay/proto"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRegisterName(t *testing.T) {
+ server := &Server{}
+ ctx := context.Background()
+
+ t.Run("Valid IP Registration", func(t *testing.T) {
+ response, err := server.RegisterName(ctx, &pb.RelayRequest{Ip: "192.168.1.1"})
+ require.NoError(t, err)
+ assert.NotEmpty(t, response.Name)
+ assert.Equal(t, "192.168.1.1", response.Ip)
+ })
+
+ t.Run("Empty IP Registration", func(t *testing.T) {
+ response, err := server.RegisterName(ctx, &pb.RelayRequest{Ip: ""})
+ assert.Error(t, err)
+ assert.Nil(t, response)
+ })
+
+}
+
+func TestLookup(t *testing.T) {
+ server := &Server{}
+ ctx := context.Background()
+
+ registerResponse, err := server.RegisterName(ctx, &pb.RelayRequest{Ip: "192.168.1.1"})
+ require.NoError(t, err)
+
+ t.Run("Successful Lookup", func(t *testing.T) {
+ response, err := server.Lookup(ctx, &pb.LookupRequest{Name: registerResponse.Name})
+ require.NoError(t, err)
+ assert.Equal(t, registerResponse.Name, response.Name)
+ assert.Equal(t, "192.168.1.1", response.Ip)
+ })
+
+ t.Run("Lookup with Empty Name", func(t *testing.T) {
+ response, err := server.Lookup(ctx, &pb.LookupRequest{Name: ""})
+ assert.Error(t, err)
+ assert.Nil(t, response)
+ })
+
+ t.Run("Lookup Non-Existent Name", func(t *testing.T) {
+ response, err := server.Lookup(ctx, &pb.LookupRequest{Name: "nonexistent"})
+ assert.Error(t, err)
+ assert.Nil(t, response)
+ })
+}
+
+func TestCloseName(t *testing.T) {
+ server := &Server{}
+ ctx := context.Background()
+
+ // Prepare a registered name
+ registerResponse, err := server.RegisterName(ctx, &pb.RelayRequest{Ip: "192.168.1.1"})
+ require.NoError(t, err)
+
+ t.Run("Successful Close", func(t *testing.T) {
+ response, err := server.CloseName(ctx, &pb.LookupRequest{Name: registerResponse.Name})
+ require.NoError(t, err)
+ assert.True(t, response.Status)
+
+ // Verify the name is no longer in the table
+ _, err = server.Lookup(ctx, &pb.LookupRequest{Name: registerResponse.Name})
+ assert.Error(t, err)
+ })
+
+ t.Run("Close with Empty Name", func(t *testing.T) {
+ _, err := server.CloseName(ctx, &pb.LookupRequest{Name: ""})
+ assert.Error(t, err)
+ })
+
+ t.Run("Close Non-Existent Name", func(t *testing.T) {
+ response, err := server.CloseName(ctx, &pb.LookupRequest{Name: "nonexistent"})
+ assert.Error(t, err)
+ assert.False(t, response.Status)
+ })
+}
diff --git a/relay/session.go b/relay/session.go
new file mode 100644
index 0000000..943c684
--- /dev/null
+++ b/relay/session.go
@@ -0,0 +1,23 @@
+package relay
+
+import (
+ "math/rand"
+)
+
+var adjectives = []string{
+ "adamant", "adept", "adventurous", "arcadian", "auspicious",
+ "awesome", "blossoming", "brave", "charming", "chatty",
+ "circular", "considerate", "cubic", "curious", "delighted",
+}
+
+var nouns = []string{
+ "aardvark", "accordion", "apple", "apricot", "bee",
+ "brachiosaur", "cactus", "capsicum", "clarinet", "cowbell",
+ "crab", "cuckoo", "cymbal", "diplodocus", "donkey",
+}
+
+func newSession() string {
+ noun := nouns[rand.Intn(len(nouns))]
+ adjective := adjectives[rand.Intn(len(adjectives))]
+ return noun + "-" + adjective
+}