diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..6885717 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/go +{ + "name": "Go", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/go:1-1.22-bookworm" + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "go version", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f33a02c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index ea8023f..25810c4 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -1,7 +1,7 @@ # This workflow will build a Go project # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go -name: ARCTIC CAMIO Lab +name: CAMIO Lab on: push: @@ -27,4 +27,4 @@ jobs: run: go build -o diode . - name: Test with the Go CLI - run: go test + run: go test -v ./... diff --git a/Makefile b/Makefile index cc12324..15171bc 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,9 @@ build: test: go test -v ./... +coverage: + go test -cover ./... + run: build ./${BIN_NAME} --help diff --git a/README.md b/README.md index 8965ba4..1ba2304 100644 --- a/README.md +++ b/README.md @@ -6,20 +6,19 @@ Scripts for verifying TCP passthrough functionality. > [!TIP] > This project utilizes [`go`](https://go.dev/) for module management. -> You can find installation instructions via https://go.dev/doc/install. +> You can find installation instructions via [relevant operating system](https://go.dev/doc/install) documentation. - Clone repository: `gh repo clone acep-uaf/data-diode` - Source navigation: `cd data-diode` -- Build binary: `make` - - [`build-essential`](https://packages.ubuntu.com/focal/build-essential) +- Build binary: [`make`](https://www.gnu.org/software/make/) - CLI: `./diode [options...]` -#### Branch Management +### Branch Management - `main` → production ready environment. - `dev` → testing changes to be merged into `main`. -#### Directory Structure +### Directory Structure ```zsh . @@ -39,7 +38,7 @@ Scripts for verifying TCP passthrough functionality. 5 directories, 7 files ``` -#### Architecture Diagram +### Architecture Diagram ```mermaid graph LR @@ -52,23 +51,23 @@ graph LR ## User Stories -#### Scenario Planning +### Scenario Planning 1. Power Plant Operator 1. Information Security Auditor 1. Energy Awareness Application Developer 1. Community Member -#### Threat Model[^1] +### Threat Model[^1] - [ ] Tactics - [ ] Techniques - [ ] Procedures -## System Benchmarking +### System Benchmarking -#### Experimental Design +### Experimental Design -###### [Device Configuration](docs/SOP.md) +#### [Device Configuration](docs/SOP.md) -[^1]: https://csrc.nist.gov/glossary/term/tactics_techniques_and_procedures +[^1]: diff --git a/docs/SOP.md b/docs/SOP.md index ae6e131..484555d 100644 --- a/docs/SOP.md +++ b/docs/SOP.md @@ -7,16 +7,16 @@ ## Research Question -#### State Synchronization +### State Synchronization -#### System Replication +### System Replication ## System Analysis -#### Latency +### Latency -#### Bandwidth +### Bandwidth -#### Throughput +### Throughput ## Data Integrity diff --git a/go.mod b/go.mod index e14fa35..5ce9e00 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,9 @@ require ( require ( github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect - github.com/gorilla/websocket v1.5.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - golang.org/x/net v0.25.0 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/sync v0.7.0 // indirect ) diff --git a/go.sum b/go.sum index b08c121..f9d1905 100644 --- a/go.sum +++ b/go.sum @@ -2,16 +2,16 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lV github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/main.go b/main.go index 6597301..3b9c290 100644 --- a/main.go +++ b/main.go @@ -131,6 +131,7 @@ func main() { Action: func(dCtx *cli.Context) error { fmt.Println("----- DIAGNOSTICS -----") fmt.Printf("%+v\n", config) + // TODO: Flag for current data diode ACK response. return nil }, }, @@ -147,7 +148,7 @@ func main() { { Name: "mqtt-subscribe", Aliases: []string{"ms"}, - Usage: "Recieve payload, encapsulate message, & stream to diode", + Usage: "Receive payload, encapsulate message, & stream to diode", Action: func(msCtx *cli.Context) error { utility.InboundMessageFlow(subBrokerIP, subBrokerPort, subBrokerTopic, clientLocation) return nil diff --git a/utility/application.go b/utility/application.go index 69fee57..36be638 100644 --- a/utility/application.go +++ b/utility/application.go @@ -13,6 +13,7 @@ const ( SAMPLE = 10240 ) +// For versions prior to Go 1.21 (built-ins). func min(a, b int) int { if a < b { return a @@ -150,7 +151,7 @@ func StartPlaceholderServer(host string, port int) { } } -func RecieveMessage(destination string, messages chan<- string) error { +func ReceiveMessage(destination string, messages chan<- string) error { server, err := net.Listen("tcp", destination) if err != nil { fmt.Println(">> [!] Error connecting to diode: ", err) diff --git a/utility/application_test.go b/utility/application_test.go index dced201..4e76bc6 100644 --- a/utility/application_test.go +++ b/utility/application_test.go @@ -5,26 +5,36 @@ import ( "testing" ) -func TestRecieveMessage(t *testing.T) { - serverMock, err := net.Listen("tcp", "localhost:0") +func TestSendMessage(t *testing.T) { + server, err := net.Listen(CONN_TYPE, "localhost:0") if err != nil { - t.Fatalf("[!] Failed to start the mock server: %v", err) + t.Fatalf(">> [!] Failed to create mock server: %v", err) } - defer serverMock.Close() + defer server.Close() - serverMockAddress := serverMock.Addr().String() - - contents := make(chan string) go func() { - err := RecieveMessage(serverMockAddress, contents) + conn, err := server.Accept() if err != nil { - t.Errorf("[?] Returned an error: %v", err) + t.Error(">> [!] Failed to accept connection:", err) + return + } + defer conn.Close() + + buffer := make([]byte, CHUNK_SIZE) + _, err = conn.Read(buffer) + if err != nil { + t.Error("[!] Failed to read data:", err) + return + } + + _, err = conn.Write([]byte(ACKNOWLEDGEMENT)) + if err != nil { + t.Error("[!] Failed to send acknowledgement:", err) + return } }() - conn, err := net.Dial("tcp", serverMockAddress) - if err != nil { - t.Fatalf("[!] Failed to connect to the mock server: %v", err) - } - defer conn.Close() + input := "data-diode" + client := server.Addr().String() + SendMessage(input, client) } diff --git a/utility/metadata.go b/utility/metadata.go new file mode 100644 index 0000000..ca556de --- /dev/null +++ b/utility/metadata.go @@ -0,0 +1,31 @@ +package utility + +import ( + "crypto/md5" + "encoding/base64" + "fmt" + "time" +) + +func MakeTimestamp() int64 { + return time.Now().UnixMicro() +} + +func Verification(data string) string { + hash := md5.New() + hash.Write([]byte(data)) + return fmt.Sprintf("%x", hash.Sum(nil)) +} + +func EncapsulatePayload(message string) string { + encoded := base64.StdEncoding.EncodeToString([]byte(message)) + return encoded +} + +func UnencapsulatePayload(message string) string { + decoded, err := base64.StdEncoding.DecodeString(message) + if err != nil { + fmt.Println(">> [!] Error decoding the message: ", err) + } + return string(decoded) +} diff --git a/utility/metadata_test.go b/utility/metadata_test.go new file mode 100644 index 0000000..a97605f --- /dev/null +++ b/utility/metadata_test.go @@ -0,0 +1,68 @@ +package utility + +import ( + "fmt" + "testing" +) + +func TestMakeTimestamp(t *testing.T) { + actual := MakeTimestamp() + expected := 16 + + length := len(fmt.Sprintf("%d", actual)) + + if length != expected { + t.Errorf("Expected %d but got %d", expected, length) + } +} + +func TestVerification(t *testing.T) { + testcases := []struct { + input string + }{ + {"data-diode"}, + } + + for _, test := range testcases { + actual := Verification(test.input) + expected := "ef217cdf54b0b3ece89a0d7686da550b" + + if actual != expected { + t.Errorf("Expected %s but got %s", expected, actual) + } + } +} + +func TestEncapsulatePayload(t *testing.T) { + testcases := []struct { + input string + }{ + {"data-diode"}, + } + + for _, test := range testcases { + actual := EncapsulatePayload(test.input) + expected := "ZGF0YS1kaW9kZQ==" + + if actual != expected { + t.Errorf("Expected %s but got %s", expected, actual) + } + } +} + +func TestUnencapsulatePayload(t *testing.T) { + testcases := []struct { + input string + }{ + {"ZGF0YS1kaW9kZQ=="}, + } + + for _, test := range testcases { + actual := UnencapsulatePayload(test.input) + expected := "data-diode" + + if actual != expected { + t.Errorf("Expected %s but got %s", expected, actual) + } + } +} diff --git a/utility/republisher.go b/utility/republisher.go index 7b016e7..8a308df 100644 --- a/utility/republisher.go +++ b/utility/republisher.go @@ -1,14 +1,11 @@ package utility import ( - "crypto/md5" - "encoding/base64" "encoding/json" "fmt" "log" "os" "os/signal" - "time" mqtt "github.com/eclipse/paho.mqtt.golang" ) @@ -42,7 +39,6 @@ func InboundMessageFlow(server string, port int, topic string, arrival string) { SendMessage(contents, arrival) } - // Subscription (Topic) if token := client.Subscribe(topic, 0, handleMessage); token.Wait() && token.Error() != nil { if token.Error() != nil { fmt.Println(">> [!] Error subscribing to the topic: ", token.Error()) @@ -55,10 +51,10 @@ func InboundMessageFlow(server string, port int, topic string, arrival string) { <-c } -func OutboundMessageFlow(server string, port int, topic string, destination string) { +func OutboundMessageFlow(server string, port int, prefix string, destination string) { messages := make(chan string) go func() { - err := RecieveMessage(destination, messages) + err := ReceiveMessage(destination, messages) if err != nil { fmt.Println(err) return @@ -66,8 +62,23 @@ func OutboundMessageFlow(server string, port int, topic string, destination stri }() for message := range messages { - repackaged := RepackageContents(message, topic) - PublishPayload(server, port, topic, repackaged) + var msg InputDiodeMessage + err := json.Unmarshal([]byte(message), &msg) + if err != nil { + fmt.Println(">> [!] Error parsing JSON message: ", err) + continue + } + + // TODO: Bounds checking for extracted top-level message intent. + + if msg.Topic == "" { + fmt.Println(">> [!] Error extracting the message topic.") + continue + } + + prepend := prefix + "/" + msg.Topic + repackaged := RepackageContents(message, prepend) + PublishPayload(server, port, prepend, repackaged) } } @@ -111,19 +122,6 @@ func RepackageContents(message string, topic string) string { return string(intermediary.Payload) } -func EncapsulatePayload(message string) string { - encoded := base64.StdEncoding.EncodeToString([]byte(message)) - return encoded -} - -func UnencapsulatePayload(message string) string { - decoded, err := base64.StdEncoding.DecodeString(message) - if err != nil { - fmt.Println(">> [!] Error decoding the message: ", err) - } - return string(decoded) -} - func PublishPayload(server string, port int, topic string, message string) { location := fmt.Sprintf("tcp://%s:%d", server, port) opts := mqtt.NewClientOptions().AddBroker(location).SetClientID("out_rec_string") @@ -137,13 +135,3 @@ func PublishPayload(server string, port int, topic string, message string) { fmt.Println(">> [!] Error publishing the message: ", token.Error()) } } - -func MakeTimestamp() int64 { - return time.Now().UnixMicro() -} - -func Verification(data string) string { - hash := md5.New() - hash.Write([]byte(data)) - return fmt.Sprintf("%x", hash.Sum(nil)) -} diff --git a/utility/republisher_test.go b/utility/republisher_test.go index 01e7efd..bff38d3 100644 --- a/utility/republisher_test.go +++ b/utility/republisher_test.go @@ -1,21 +1,77 @@ package utility -import "testing" +import ( + "encoding/json" + "fmt" + "testing" +) -func TestSubscribe(t *testing.T) { - got := "pub" - want := "sub" +func TestDetectContents(t *testing.T) { + testcases := []struct { + input string + topic string + }{ + {"data-diode", "first"}, + {"99775", "second"}, + {"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum", "third"}, + } + + for _, test := range testcases { + actualJSON := DetectContents(test.input, test.topic) + expectedJSON := fmt.Sprintf(`{"time": %d, "topic": "%s", "b64payload": "%s"}`, MakeTimestamp(), test.topic, EncapsulatePayload(test.input)) + + var actual, expected map[string]interface{} + if err := json.Unmarshal([]byte(actualJSON), &actual); err != nil { + t.Errorf(">> [!] Failed to unmarshal the actual output: %v", err) + } + if err := json.Unmarshal([]byte(expectedJSON), &expected); err != nil { + t.Errorf(">> [!] Failed to unmarshal the expected output: %v", err) + } + + // TODO: Unique timestamp comparison values with appropriate precision. - if got != want { - t.Errorf("got %q, want %q", got, want) + delete(actual, "time") + delete(expected, "time") + + actualStr, _ := json.Marshal(actual) + expectedStr, _ := json.Marshal(expected) + + if string(actualStr) != string(expectedStr) { + t.Errorf("Expected %s but got %s", expectedStr, actualStr) + } } } -func TestPublish(t *testing.T) { - got := "sub" - want := "pub" +func TestRepackageContents(t *testing.T) { + testcases := []struct { + input string + topic string + output string + }{ + {"{\"time\": 1234567890123456, \"topic\": \"test-topic\", \"b64payload\": \"ZGF0YS1kaW9kZQ==\"}", "test-topic", "data-diode"}, + } + + for _, test := range testcases { + actual := RepackageContents(test.input, test.topic) + expected := test.output + + if actual != expected { + t.Errorf("Expected %s but got %s", expected, actual) + } + } +} + +func TestPublishPayload(t *testing.T) { + testcases := []struct { + server string + port int + topic string + message string + }{ + {"localhost", 1883, "test-topic", "data-diode"}, + } - if got != want { - t.Errorf("got %q, want %q", got, want) + for _, test := range testcases { + PublishPayload(test.server, test.port, test.topic, test.message) } }