diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..0143561c0980d9e42f1557b9cef488a5e8bfd8b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +/coverage/ +/bin/ + + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/modules.xml +# .idea/*.iml +# .idea/modules + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..2b52474da669ac755b7e01bef6ce937e66c11693 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,42 @@ +image: golang:1.16-buster + +cache: + paths: + - /apt-cache + - /go/src/github.com + - /go/src/golang.org + - /go/src/google.golang.org + - /go/src/gopkg.in + +stages: + - test + +before_script: + +unit_tests: + stage: test + script: + - make test + only: + - merge_requests + +race_detector: + stage: test + script: + - make test-race + only: + - merge_requests + +code_coverage: + stage: test + script: + - make coverage + only: + - merge_requests + +lint_code: + stage: test + script: + - make lint + only: + - merge_requests diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..73f69e0958611ac6e00bde95641f6699030ad235 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/aws.xml b/.idea/aws.xml new file mode 100644 index 0000000000000000000000000000000000000000..ec328d0bbf68db9e7322932181cc811412e3ca87 --- /dev/null +++ b/.idea/aws.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="accountSettings"> + <option name="activeProfile" value="profile:default" /> + <option name="activeRegion" value="eu-west-1" /> + <option name="recentlyUsedProfiles"> + <list> + <option value="profile:default" /> + </list> + </option> + <option name="recentlyUsedRegions"> + <list> + <option value="eu-west-1" /> + </list> + </option> + </component> +</project> \ No newline at end of file diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml new file mode 100644 index 0000000000000000000000000000000000000000..ea639c3e6909c3231f0285933b4f83a0e2e69c6d --- /dev/null +++ b/.idea/dbnavigator.xml @@ -0,0 +1,463 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="DBNavigator.Project.DataEditorManager"> + <record-view-column-sorting-type value="BY_INDEX" /> + <value-preview-text-wrapping value="true" /> + <value-preview-pinned value="false" /> + </component> + <component name="DBNavigator.Project.DataExportManager"> + <export-instructions> + <create-header value="true" /> + <friendly-headers value="false" /> + <quote-values-containing-separator value="true" /> + <quote-all-values value="false" /> + <value-separator value="" /> + <file-name value="" /> + <file-location value="" /> + <scope value="GLOBAL" /> + <destination value="FILE" /> + <format value="EXCEL" /> + <charset value="UTF-8" /> + </export-instructions> + </component> + <component name="DBNavigator.Project.DatabaseBrowserManager"> + <autoscroll-to-editor value="false" /> + <autoscroll-from-editor value="true" /> + <show-object-properties value="true" /> + <loaded-nodes /> + </component> + <component name="DBNavigator.Project.DatabaseFileManager"> + <open-files /> + </component> + <component name="DBNavigator.Project.EditorStateManager"> + <last-used-providers /> + </component> + <component name="DBNavigator.Project.ExecutionManager"> + <retain-sticky-names value="false" /> + </component> + <component name="DBNavigator.Project.MethodExecutionManager"> + <method-browser /> + <execution-history> + <group-entries value="true" /> + <execution-inputs /> + </execution-history> + <argument-values-cache /> + </component> + <component name="DBNavigator.Project.ObjectDependencyManager"> + <last-used-dependency-type value="INCOMING" /> + </component> + <component name="DBNavigator.Project.ObjectQuickFilterManager"> + <last-used-operator value="EQUAL" /> + <filters /> + </component> + <component name="DBNavigator.Project.ScriptExecutionManager" clear-outputs="true"> + <recently-used-interfaces /> + </component> + <component name="DBNavigator.Project.Settings"> + <connections /> + <browser-settings> + <general> + <display-mode value="TABBED" /> + <navigation-history-size value="100" /> + <show-object-details value="false" /> + </general> + <filters> + <object-type-filter> + <object-type name="SCHEMA" enabled="true" /> + <object-type name="USER" enabled="true" /> + <object-type name="ROLE" enabled="true" /> + <object-type name="PRIVILEGE" enabled="true" /> + <object-type name="CHARSET" enabled="true" /> + <object-type name="TABLE" enabled="true" /> + <object-type name="VIEW" enabled="true" /> + <object-type name="MATERIALIZED_VIEW" enabled="true" /> + <object-type name="NESTED_TABLE" enabled="true" /> + <object-type name="COLUMN" enabled="true" /> + <object-type name="INDEX" enabled="true" /> + <object-type name="CONSTRAINT" enabled="true" /> + <object-type name="DATASET_TRIGGER" enabled="true" /> + <object-type name="DATABASE_TRIGGER" enabled="true" /> + <object-type name="SYNONYM" enabled="true" /> + <object-type name="SEQUENCE" enabled="true" /> + <object-type name="PROCEDURE" enabled="true" /> + <object-type name="FUNCTION" enabled="true" /> + <object-type name="PACKAGE" enabled="true" /> + <object-type name="TYPE" enabled="true" /> + <object-type name="TYPE_ATTRIBUTE" enabled="true" /> + <object-type name="ARGUMENT" enabled="true" /> + <object-type name="DIMENSION" enabled="true" /> + <object-type name="CLUSTER" enabled="true" /> + <object-type name="DBLINK" enabled="true" /> + </object-type-filter> + </filters> + <sorting> + <object-type name="COLUMN" sorting-type="NAME" /> + <object-type name="FUNCTION" sorting-type="NAME" /> + <object-type name="PROCEDURE" sorting-type="NAME" /> + <object-type name="ARGUMENT" sorting-type="POSITION" /> + </sorting> + <default-editors> + <object-type name="VIEW" editor-type="SELECTION" /> + <object-type name="PACKAGE" editor-type="SELECTION" /> + <object-type name="TYPE" editor-type="SELECTION" /> + </default-editors> + </browser-settings> + <navigation-settings> + <lookup-filters> + <lookup-objects> + <object-type name="SCHEMA" enabled="true" /> + <object-type name="USER" enabled="false" /> + <object-type name="ROLE" enabled="false" /> + <object-type name="PRIVILEGE" enabled="false" /> + <object-type name="CHARSET" enabled="false" /> + <object-type name="TABLE" enabled="true" /> + <object-type name="VIEW" enabled="true" /> + <object-type name="MATERIALIZED VIEW" enabled="true" /> + <object-type name="INDEX" enabled="true" /> + <object-type name="CONSTRAINT" enabled="true" /> + <object-type name="DATASET TRIGGER" enabled="true" /> + <object-type name="DATABASE TRIGGER" enabled="true" /> + <object-type name="SYNONYM" enabled="false" /> + <object-type name="SEQUENCE" enabled="true" /> + <object-type name="PROCEDURE" enabled="true" /> + <object-type name="FUNCTION" enabled="true" /> + <object-type name="PACKAGE" enabled="true" /> + <object-type name="TYPE" enabled="true" /> + <object-type name="DIMENSION" enabled="false" /> + <object-type name="CLUSTER" enabled="false" /> + <object-type name="DBLINK" enabled="true" /> + </lookup-objects> + <force-database-load value="false" /> + <prompt-connection-selection value="true" /> + <prompt-schema-selection value="true" /> + </lookup-filters> + </navigation-settings> + <dataset-grid-settings> + <general> + <enable-zooming value="true" /> + <enable-column-tooltip value="true" /> + </general> + <sorting> + <nulls-first value="true" /> + <max-sorting-columns value="4" /> + </sorting> + <tracking-columns> + <columnNames value="" /> + <visible value="true" /> + <editable value="false" /> + </tracking-columns> + </dataset-grid-settings> + <dataset-editor-settings> + <text-editor-popup> + <active value="false" /> + <active-if-empty value="false" /> + <data-length-threshold value="100" /> + <popup-delay value="1000" /> + </text-editor-popup> + <values-actions-popup> + <show-popup-button value="true" /> + <element-count-threshold value="1000" /> + <data-length-threshold value="250" /> + </values-actions-popup> + <general> + <fetch-block-size value="100" /> + <fetch-timeout value="30" /> + <trim-whitespaces value="true" /> + <convert-empty-strings-to-null value="true" /> + <select-content-on-cell-edit value="true" /> + <large-value-preview-active value="true" /> + </general> + <filters> + <prompt-filter-dialog value="true" /> + <default-filter-type value="BASIC" /> + </filters> + <qualified-text-editor text-length-threshold="300"> + <content-types> + <content-type name="Text" enabled="true" /> + <content-type name="Properties" enabled="true" /> + <content-type name="XML" enabled="true" /> + <content-type name="DTD" enabled="true" /> + <content-type name="HTML" enabled="true" /> + <content-type name="XHTML" enabled="true" /> + <content-type name="CSS" enabled="true" /> + <content-type name="Java" enabled="true" /> + <content-type name="SQL" enabled="true" /> + <content-type name="PL/SQL" enabled="true" /> + <content-type name="JavaScript" enabled="true" /> + <content-type name="JSON" enabled="true" /> + <content-type name="JSON5" enabled="true" /> + <content-type name="PHP" enabled="true" /> + <content-type name="JSP" enabled="true" /> + <content-type name="JSPx" enabled="true" /> + <content-type name="FTL" enabled="true" /> + <content-type name="VTL" enabled="true" /> + <content-type name="YAML" enabled="true" /> + <content-type name="Manifest" enabled="true" /> + </content-types> + </qualified-text-editor> + <record-navigation> + <navigation-target value="VIEWER" /> + </record-navigation> + </dataset-editor-settings> + <code-editor-settings> + <general> + <show-object-navigation-gutter value="false" /> + <show-spec-declaration-navigation-gutter value="true" /> + <enable-spellchecking value="true" /> + <enable-reference-spellchecking value="false" /> + </general> + <confirmations> + <save-changes value="false" /> + <revert-changes value="true" /> + </confirmations> + </code-editor-settings> + <code-completion-settings> + <filters> + <basic-filter> + <filter-element type="RESERVED_WORD" id="keyword" selected="true" /> + <filter-element type="RESERVED_WORD" id="function" selected="true" /> + <filter-element type="RESERVED_WORD" id="parameter" selected="true" /> + <filter-element type="RESERVED_WORD" id="datatype" selected="true" /> + <filter-element type="RESERVED_WORD" id="exception" selected="true" /> + <filter-element type="OBJECT" id="schema" selected="true" /> + <filter-element type="OBJECT" id="role" selected="true" /> + <filter-element type="OBJECT" id="user" selected="true" /> + <filter-element type="OBJECT" id="privilege" selected="true" /> + <user-schema> + <filter-element type="OBJECT" id="table" selected="true" /> + <filter-element type="OBJECT" id="view" selected="true" /> + <filter-element type="OBJECT" id="materialized view" selected="true" /> + <filter-element type="OBJECT" id="index" selected="true" /> + <filter-element type="OBJECT" id="constraint" selected="true" /> + <filter-element type="OBJECT" id="trigger" selected="true" /> + <filter-element type="OBJECT" id="synonym" selected="false" /> + <filter-element type="OBJECT" id="sequence" selected="true" /> + <filter-element type="OBJECT" id="procedure" selected="true" /> + <filter-element type="OBJECT" id="function" selected="true" /> + <filter-element type="OBJECT" id="package" selected="true" /> + <filter-element type="OBJECT" id="type" selected="true" /> + <filter-element type="OBJECT" id="dimension" selected="true" /> + <filter-element type="OBJECT" id="cluster" selected="true" /> + <filter-element type="OBJECT" id="dblink" selected="true" /> + </user-schema> + <public-schema> + <filter-element type="OBJECT" id="table" selected="false" /> + <filter-element type="OBJECT" id="view" selected="false" /> + <filter-element type="OBJECT" id="materialized view" selected="false" /> + <filter-element type="OBJECT" id="index" selected="false" /> + <filter-element type="OBJECT" id="constraint" selected="false" /> + <filter-element type="OBJECT" id="trigger" selected="false" /> + <filter-element type="OBJECT" id="synonym" selected="false" /> + <filter-element type="OBJECT" id="sequence" selected="false" /> + <filter-element type="OBJECT" id="procedure" selected="false" /> + <filter-element type="OBJECT" id="function" selected="false" /> + <filter-element type="OBJECT" id="package" selected="false" /> + <filter-element type="OBJECT" id="type" selected="false" /> + <filter-element type="OBJECT" id="dimension" selected="false" /> + <filter-element type="OBJECT" id="cluster" selected="false" /> + <filter-element type="OBJECT" id="dblink" selected="false" /> + </public-schema> + <any-schema> + <filter-element type="OBJECT" id="table" selected="true" /> + <filter-element type="OBJECT" id="view" selected="true" /> + <filter-element type="OBJECT" id="materialized view" selected="true" /> + <filter-element type="OBJECT" id="index" selected="true" /> + <filter-element type="OBJECT" id="constraint" selected="true" /> + <filter-element type="OBJECT" id="trigger" selected="true" /> + <filter-element type="OBJECT" id="synonym" selected="true" /> + <filter-element type="OBJECT" id="sequence" selected="true" /> + <filter-element type="OBJECT" id="procedure" selected="true" /> + <filter-element type="OBJECT" id="function" selected="true" /> + <filter-element type="OBJECT" id="package" selected="true" /> + <filter-element type="OBJECT" id="type" selected="true" /> + <filter-element type="OBJECT" id="dimension" selected="true" /> + <filter-element type="OBJECT" id="cluster" selected="true" /> + <filter-element type="OBJECT" id="dblink" selected="true" /> + </any-schema> + </basic-filter> + <extended-filter> + <filter-element type="RESERVED_WORD" id="keyword" selected="true" /> + <filter-element type="RESERVED_WORD" id="function" selected="true" /> + <filter-element type="RESERVED_WORD" id="parameter" selected="true" /> + <filter-element type="RESERVED_WORD" id="datatype" selected="true" /> + <filter-element type="RESERVED_WORD" id="exception" selected="true" /> + <filter-element type="OBJECT" id="schema" selected="true" /> + <filter-element type="OBJECT" id="user" selected="true" /> + <filter-element type="OBJECT" id="role" selected="true" /> + <filter-element type="OBJECT" id="privilege" selected="true" /> + <user-schema> + <filter-element type="OBJECT" id="table" selected="true" /> + <filter-element type="OBJECT" id="view" selected="true" /> + <filter-element type="OBJECT" id="materialized view" selected="true" /> + <filter-element type="OBJECT" id="index" selected="true" /> + <filter-element type="OBJECT" id="constraint" selected="true" /> + <filter-element type="OBJECT" id="trigger" selected="true" /> + <filter-element type="OBJECT" id="synonym" selected="true" /> + <filter-element type="OBJECT" id="sequence" selected="true" /> + <filter-element type="OBJECT" id="procedure" selected="true" /> + <filter-element type="OBJECT" id="function" selected="true" /> + <filter-element type="OBJECT" id="package" selected="true" /> + <filter-element type="OBJECT" id="type" selected="true" /> + <filter-element type="OBJECT" id="dimension" selected="true" /> + <filter-element type="OBJECT" id="cluster" selected="true" /> + <filter-element type="OBJECT" id="dblink" selected="true" /> + </user-schema> + <public-schema> + <filter-element type="OBJECT" id="table" selected="true" /> + <filter-element type="OBJECT" id="view" selected="true" /> + <filter-element type="OBJECT" id="materialized view" selected="true" /> + <filter-element type="OBJECT" id="index" selected="true" /> + <filter-element type="OBJECT" id="constraint" selected="true" /> + <filter-element type="OBJECT" id="trigger" selected="true" /> + <filter-element type="OBJECT" id="synonym" selected="true" /> + <filter-element type="OBJECT" id="sequence" selected="true" /> + <filter-element type="OBJECT" id="procedure" selected="true" /> + <filter-element type="OBJECT" id="function" selected="true" /> + <filter-element type="OBJECT" id="package" selected="true" /> + <filter-element type="OBJECT" id="type" selected="true" /> + <filter-element type="OBJECT" id="dimension" selected="true" /> + <filter-element type="OBJECT" id="cluster" selected="true" /> + <filter-element type="OBJECT" id="dblink" selected="true" /> + </public-schema> + <any-schema> + <filter-element type="OBJECT" id="table" selected="true" /> + <filter-element type="OBJECT" id="view" selected="true" /> + <filter-element type="OBJECT" id="materialized view" selected="true" /> + <filter-element type="OBJECT" id="index" selected="true" /> + <filter-element type="OBJECT" id="constraint" selected="true" /> + <filter-element type="OBJECT" id="trigger" selected="true" /> + <filter-element type="OBJECT" id="synonym" selected="true" /> + <filter-element type="OBJECT" id="sequence" selected="true" /> + <filter-element type="OBJECT" id="procedure" selected="true" /> + <filter-element type="OBJECT" id="function" selected="true" /> + <filter-element type="OBJECT" id="package" selected="true" /> + <filter-element type="OBJECT" id="type" selected="true" /> + <filter-element type="OBJECT" id="dimension" selected="true" /> + <filter-element type="OBJECT" id="cluster" selected="true" /> + <filter-element type="OBJECT" id="dblink" selected="true" /> + </any-schema> + </extended-filter> + </filters> + <sorting enabled="true"> + <sorting-element type="RESERVED_WORD" id="keyword" /> + <sorting-element type="RESERVED_WORD" id="datatype" /> + <sorting-element type="OBJECT" id="column" /> + <sorting-element type="OBJECT" id="table" /> + <sorting-element type="OBJECT" id="view" /> + <sorting-element type="OBJECT" id="materialized view" /> + <sorting-element type="OBJECT" id="index" /> + <sorting-element type="OBJECT" id="constraint" /> + <sorting-element type="OBJECT" id="trigger" /> + <sorting-element type="OBJECT" id="synonym" /> + <sorting-element type="OBJECT" id="sequence" /> + <sorting-element type="OBJECT" id="procedure" /> + <sorting-element type="OBJECT" id="function" /> + <sorting-element type="OBJECT" id="package" /> + <sorting-element type="OBJECT" id="type" /> + <sorting-element type="OBJECT" id="dimension" /> + <sorting-element type="OBJECT" id="cluster" /> + <sorting-element type="OBJECT" id="dblink" /> + <sorting-element type="OBJECT" id="schema" /> + <sorting-element type="OBJECT" id="role" /> + <sorting-element type="OBJECT" id="user" /> + <sorting-element type="RESERVED_WORD" id="function" /> + <sorting-element type="RESERVED_WORD" id="parameter" /> + </sorting> + <format> + <enforce-code-style-case value="true" /> + </format> + </code-completion-settings> + <execution-engine-settings> + <statement-execution> + <fetch-block-size value="100" /> + <execution-timeout value="20" /> + <debug-execution-timeout value="600" /> + <focus-result value="false" /> + <prompt-execution value="false" /> + </statement-execution> + <script-execution> + <command-line-interfaces /> + <execution-timeout value="300" /> + </script-execution> + <method-execution> + <execution-timeout value="30" /> + <debug-execution-timeout value="600" /> + <parameter-history-size value="10" /> + </method-execution> + </execution-engine-settings> + <operation-settings> + <transactions> + <uncommitted-changes> + <on-project-close value="ASK" /> + <on-disconnect value="ASK" /> + <on-autocommit-toggle value="ASK" /> + </uncommitted-changes> + <multiple-uncommitted-changes> + <on-commit value="ASK" /> + <on-rollback value="ASK" /> + </multiple-uncommitted-changes> + </transactions> + <session-browser> + <disconnect-session value="ASK" /> + <kill-session value="ASK" /> + <reload-on-filter-change value="false" /> + </session-browser> + <compiler> + <compile-type value="KEEP" /> + <compile-dependencies value="ASK" /> + <always-show-controls value="false" /> + </compiler> + <debugger> + <debugger-type value="ASK" /> + <use-generic-runners value="true" /> + </debugger> + </operation-settings> + <ddl-file-settings> + <extensions> + <mapping file-type-id="VIEW" extensions="vw" /> + <mapping file-type-id="TRIGGER" extensions="trg" /> + <mapping file-type-id="PROCEDURE" extensions="prc" /> + <mapping file-type-id="FUNCTION" extensions="fnc" /> + <mapping file-type-id="PACKAGE" extensions="pkg" /> + <mapping file-type-id="PACKAGE_SPEC" extensions="pks" /> + <mapping file-type-id="PACKAGE_BODY" extensions="pkb" /> + <mapping file-type-id="TYPE" extensions="tpe" /> + <mapping file-type-id="TYPE_SPEC" extensions="tps" /> + <mapping file-type-id="TYPE_BODY" extensions="tpb" /> + </extensions> + <general> + <lookup-ddl-files value="true" /> + <create-ddl-files value="false" /> + <synchronize-ddl-files value="true" /> + <use-qualified-names value="false" /> + <make-scripts-rerunnable value="true" /> + </general> + </ddl-file-settings> + <general-settings> + <regional-settings> + <date-format value="MEDIUM" /> + <number-format value="UNGROUPED" /> + <locale value="SYSTEM_DEFAULT" /> + <use-custom-formats value="false" /> + </regional-settings> + <environment> + <environment-types> + <environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" /> + <environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" /> + <environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" /> + <environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" /> + </environment-types> + <visibility-settings> + <connection-tabs value="true" /> + <dialog-headers value="true" /> + <object-editor-tabs value="true" /> + <script-editor-tabs value="false" /> + <execution-result-tabs value="true" /> + </visibility-settings> + </environment> + </general-settings> + </component> + <component name="DBNavigator.Project.StatementExecutionManager"> + <execution-variables /> + </component> +</project> \ No newline at end of file diff --git a/.idea/markdown-navigator-enh.xml b/.idea/markdown-navigator-enh.xml new file mode 100644 index 0000000000000000000000000000000000000000..a8fcc84db3668cadd75348be61bda65a8fc5ea21 --- /dev/null +++ b/.idea/markdown-navigator-enh.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="MarkdownEnhProjectSettings"> + <AnnotatorSettings targetHasSpaces="true" linkCaseMismatch="true" wikiCaseMismatch="true" wikiLinkHasDashes="true" notUnderWikiHome="true" targetNotWikiPageExt="true" notUnderSourceWikiHome="true" targetNameHasAnchor="true" targetPathHasAnchor="true" wikiLinkHasSlash="true" wikiLinkHasSubdir="true" wikiLinkHasOnlyAnchor="true" linkTargetsWikiHasExt="true" linkTargetsWikiHasBadExt="true" notUnderSameRepo="true" targetNotUnderVcs="false" linkNeedsExt="true" linkHasBadExt="true" linkTargetNeedsExt="true" linkTargetHasBadExt="true" wikiLinkNotInWiki="true" imageTargetNotInRaw="true" repoRelativeAcrossVcsRoots="true" multipleWikiTargetsMatch="true" unresolvedLinkReference="true" linkIsIgnored="true" anchorIsIgnored="true" anchorIsUnresolved="true" anchorLineReferenceIsUnresolved="true" anchorLineReferenceFormat="true" anchorHasDuplicates="true" abbreviationDuplicates="true" abbreviationNotUsed="true" attributeIdDuplicateDefinition="true" attributeIdNotUsed="true" footnoteDuplicateDefinition="true" footnoteUnresolved="true" footnoteDuplicates="true" footnoteNotUsed="true" macroDuplicateDefinition="true" macroUnresolved="true" macroDuplicates="true" macroNotUsed="true" referenceDuplicateDefinition="true" referenceUnresolved="true" referenceDuplicates="true" referenceNotUsed="true" referenceUnresolvedNumericId="true" enumRefDuplicateDefinition="true" enumRefUnresolved="true" enumRefDuplicates="true" enumRefNotUsed="true" enumRefLinkUnresolved="true" enumRefLinkDuplicates="true" simTocUpdateNeeded="true" simTocTitleSpaceNeeded="true" /> + <HtmlExportSettings updateOnSave="false" parentDir="" targetDir="" cssDir="css" scriptDir="js" plainHtml="false" imageDir="" copyLinkedImages="false" imagePathType="0" targetPathType="2" targetExt="" useTargetExt="false" noCssNoScripts="false" useElementStyleAttribute="false" linkToExportedHtml="true" exportOnSettingsChange="true" regenerateOnProjectOpen="false" linkFormatType="HTTP_ABSOLUTE" /> + <LinkMapSettings> + <textMaps /> + </LinkMapSettings> + </component> +</project> \ No newline at end of file diff --git a/.idea/markdown-navigator.xml b/.idea/markdown-navigator.xml new file mode 100644 index 0000000000000000000000000000000000000000..a2fc0864e7bc79d88eaf4d3f18785833dfe9d367 --- /dev/null +++ b/.idea/markdown-navigator.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="MarkdownProjectSettings"> + <PreviewSettings splitEditorLayout="SPLIT" splitEditorPreview="PREVIEW" useGrayscaleRendering="false" zoomFactor="1.0" maxImageWidth="0" synchronizePreviewPosition="true" highlightPreviewType="LINE" highlightFadeOut="5" highlightOnTyping="true" synchronizeSourcePosition="true" verticallyAlignSourceAndPreviewSyncPosition="true" showSearchHighlightsInPreview="true" showSelectionInPreview="true" lastLayoutSetsDefault="false"> + <PanelProvider> + <provider providerId="com.vladsch.md.nav.editor.swing.html.panel" providerName="Default - Swing" /> + </PanelProvider> + </PreviewSettings> + <ParserSettings gitHubSyntaxChange="false" correctedInvalidSettings="false" emojiShortcuts="1" emojiImages="0"> + <PegdownExtensions> + <option name="ATXHEADERSPACE" value="true" /> + <option name="FENCED_CODE_BLOCKS" value="true" /> + <option name="INTELLIJ_DUMMY_IDENTIFIER" value="true" /> + <option name="RELAXEDHRULES" value="true" /> + <option name="STRIKETHROUGH" value="true" /> + <option name="TABLES" value="true" /> + <option name="TASKLISTITEMS" value="true" /> + </PegdownExtensions> + <ParserOptions> + <option name="COMMONMARK_LISTS" value="true" /> + <option name="EMOJI_SHORTCUTS" value="true" /> + <option name="GFM_TABLE_RENDERING" value="true" /> + <option name="PRODUCTION_SPEC_PARSER" value="true" /> + <option name="SIM_TOC_BLANK_LINE_SPACER" value="true" /> + </ParserOptions> + </ParserSettings> + <HtmlSettings headerTopEnabled="false" headerBottomEnabled="false" bodyTopEnabled="false" bodyBottomEnabled="false" addPageHeader="false" addAnchorLinks="false" anchorLinksWrapText="false" imageUriSerials="false" addDocTypeHtml="true" noParaTags="false" defaultUrlTitle="false" migratedPlantUml="true" migratedAnchorLinks="true" plantUmlConversion="0"> + <GeneratorProvider> + <provider providerId="com.vladsch.md.nav.editor.text.html.generator" providerName="Unmodified HTML Generator" /> + </GeneratorProvider> + <headerTop /> + <headerBottom /> + <bodyTop /> + <bodyBottom /> + <fencedCodeConversions> + <option name="c4plantuml" value="NONE" /> + <option name="ditaa" value="NONE" /> + <option name="erd" value="NONE" /> + <option name="graphviz" value="NONE" /> + <option name="latex" value="KATEX" /> + <option name="math" value="KATEX" /> + <option name="mermaid" value="NONE" /> + <option name="nomnoml" value="NONE" /> + <option name="plantuml" value="NONE" /> + <option name="puml" value="NONE" /> + <option name="svgbob" value="NONE" /> + <option name="umlet" value="NONE" /> + <option name="vega" value="NONE" /> + <option name="vegalite" value="NONE" /> + <option name="wavedrom" value="NONE" /> + </fencedCodeConversions> + </HtmlSettings> + <CssSettings previewScheme="UI_SCHEME" cssUri="" isCssUriEnabled="false" isCssUriSerial="true" isCssTextEnabled="false" isDynamicPageWidth="true"> + <StylesheetProvider> + <provider providerId="com.vladsch.md.nav.editor.text.html.css" providerName="No Stylesheet" /> + </StylesheetProvider> + <ScriptProviders /> + <cssText /> + <cssUriHistory /> + </CssSettings> + </component> +</project> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000000000000000000000000000000000..639900d13c6182e452e33a3bd638e70a0146c785 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectRootManager"> + <output url="file://$PROJECT_DIR$/out" /> + </component> +</project> \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000000000000000000000000000000000..f0ad2f76bc8b29cb0a6001f9372e71a889a790f3 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectModuleManager"> + <modules> + <module fileurl="file://$PROJECT_DIR$/traceroute.iml" filepath="$PROJECT_DIR$/traceroute.iml" /> + </modules> + </component> +</project> \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000000000000000000000000000000000..35eb1ddfbbc029bcab630581847471d7f238ec53 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="" vcs="Git" /> + </component> +</project> \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..e74cfa6fcae11a1fbe280a77e044309d8867c806 --- /dev/null +++ b/Makefile @@ -0,0 +1,81 @@ + +#PACKAGES ?= $(shell basename `go list`) +#FILES ?= $(shell find . -type f -name '*.go' -not -path "./vendor/*") + +#VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2> /dev/null || cat $(PWD)/.version 2> /dev/null || echo v0) +##PACKAGE ?= $(shell go list) +##PACKAGES ?= $(shell go list ./...) + +PROJECT_NAME := "traceroute" +PKG := "gitlab.schukai.com/oss/libraries/go/network/traceroute" +PKG_LIST := $(shell go list ${PKG}/... ) +GO_FILES := $(shell find . -name '*.go') +#GO_FILES := $(shell find . -name '*.go' | grep -v _test.go) + +GOCMD=go +GOBUILD=$(GOCMD) build +GOCLEAN=$(GOCMD) clean +GOTEST=$(GOCMD) test +GOVET=$(GOCMD) vet +GOGET=$(GOCMD) get +GOFMT=$(GOCMD) fmt +GODOC=$(GOCMD) fmt +GOLINT=golint + +default: help + +help: ## show this help + @echo 'usage: make [target] ...' + @echo '' + @echo 'targets:' + @egrep '^(.+)\:\ .*##\ (.+)' ${MAKEFILE_LIST} | sed 's/:.*##/#/' | column -t -c 2 -s '#' + +clean: ## go clean + $(GOCLEAN) + +clean-all: ## remove all generated artifacts and clean all build artifacts + $(GOCLEAN) -i ./... + +lint: ## run go lint on the source files + $(GOLINT) -set_exit_status . + +vet: ## run go vet on the source files + $(GOVET) ./... + +doc: ## generate godocs and start a local documentation webserver on port 8085 + godoc -http=:8085 -index + +test: ## test package + $(GOTEST) -v $(PKG_LIST) + +test-race: ## run race-tests + $(GOTEST) -race $(PKG_LIST) + +benchmark: ## run benchmark tests + $(GOTEST) -bench . + +coverage: coverage/cover.html ## Run test coverage and generate html report + +coverage/cover.html: $(GO_FILES) + mkdir -p coverage + $(GOCMD) list -f '{{if gt (len .TestGoFiles) 0}}"go test -covermode count -coverprofile {{.Name}}.coverprofile -coverpkg ./... {{.ImportPath}}"{{end}}' ./... | xargs -I {} bash -c {} + echo "mode: count" > coverage/cover.out + grep -h -v "^mode:" *.coverprofile >> "coverage/cover.out" + rm *.coverprofile + $(GOCMD) tool cover -html=coverage/cover.out -o=coverage/cover.html + +test-all: test test-race benchmark coverage + + +fmt: ## format the go source files + $(GOFMT) . + +all: ## clean, format and unit test + make clean-all + make fmt + make test-all + +build-cmd: + mkdir -p bin + $(GOCMD) build -o bin/traceroute cmd/main.go + sudo setcap cap_net_raw+ep bin/traceroute diff --git a/README.md b/README.md index 22447bc549c2d6e06e0434f953397cb452c49320..f4658127d5f420f0965cd4362ade1a2c7ddf20f9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,113 @@ -# traceroute +# Traceroute + +This library allows to record a traceroute. + +## Requirements + +To execute the traceroute, a privileged port must be opened. this +requires special permissions. One possibility is . + +A prerequisite for reading incoming traffic is being able to either to +be root or set the executable capability `CAP_NET_RAW`. + +Reminder: You can set the `CAP_NET_RAW` capability on an executable like +this: + +```bash +sudo setcap cap_net_raw+ep /path/to/executable +``` + +Look at manpage for [setcap](https://linux.die.net/man/8/setcap) + +You can also change the restrictions on unprivileged ports. + +```bash +sudo /sbin/sysctl -w net.ipv4.ip_unprivileged_port_start=0 +``` + +for the purpose of debugging, pkttyagent can also be taken. + +```bash + +# PID is the processid +pkttyagent --process PID-OF-IDE + +## für netbeans +pkttyagent --process $(ps -xa | grep "netbeans" ) + +## für intellij +pkttyagent --process $(ps -xa | grep "IntelliJ-IDEA-Ultimate/jbr/bin/java" ) +``` + +## Installation + +The recommended way to install this package is + +```bash +go get gitlab.schukai.com/oss/libraries/go/network/traceroute +``` + +## Usage + +First, a session must be created `traceroute.NewSession(host)`. Then the +traceroute can be executed `session.TraceRoute()`. + +Either waits for the result and continues working with it, or +Specifies a callback that is called directly after a hop. + + +```go +package main + +import ( + "flag" + "fmt" + "gitlab.schukai.com/oss/libraries/go/network/traceroute" +) + +const ( + flagValue = "" + flagUsage = "hostname or ip address" +) + +func main() { + + var host string + + flag.StringVar(&host, "h", flagValue, flagUsage+" (shorthand)") + flag.StringVar(&host, "host", flagValue, flagUsage) + flag.Parse() + + if host == "" { + fmt.Println("missing host") + flag.Usage() + return + } + + session, err := traceroute.NewSession(host) + if err != nil { + flag.Usage() + return + } + + session.CallBack = func(result traceroute.Result) { + if result.Err!=nil { + fmt.Printf("%v\t%s\t\t\t\t%v\t\t(%s)\n", result.Hop, result.Station, result.Latency, result.Err.Error()) + return + } + + fmt.Printf("%v\t%s\t\t\t%v\n", result.Hop, result.Station, result.Latency) + } + + _, err=session.TraceRoute() + if err!=nil { + fmt.Println(err.Error()) + } + +} +``` + + + + diff --git a/bin/traceroute b/bin/traceroute new file mode 100755 index 0000000000000000000000000000000000000000..d1dab4a834876711bf6d28747b0700893b2a9bad Binary files /dev/null and b/bin/traceroute differ diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000000000000000000000000000000000000..f623e6cbf2a1c02ebbc7b0070934c90b4066aca4 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "flag" + "fmt" + "gitlab.schukai.com/oss/libraries/go/network/traceroute" +) + +const ( + flagValue = "" + flagUsage = "hostname or ip address" +) + +func main() { + + var host string + + flag.StringVar(&host, "h", flagValue, flagUsage+" (shorthand)") + flag.StringVar(&host, "host", flagValue, flagUsage) + flag.Parse() + + if host == "" { + fmt.Println("missing host") + flag.Usage() + return + } + + session, err := traceroute.NewSession(host) + if err != nil { + flag.Usage() + return + } + + session.CallBack = func(result traceroute.Result) { + if result.Err!=nil { + fmt.Printf("%v\t%s\t\t\t\t%v\t\t(%s)\n", result.Hop, result.Station, result.Latency, result.Err.Error()) + return + } + + fmt.Printf("%v\t%s\t\t\t%v\n", result.Hop, result.Station, result.Latency) + } + + _, err=session.TraceRoute() + if err!=nil { + fmt.Println(err.Error()) + } + +} diff --git a/dns.go b/dns.go new file mode 100644 index 0000000000000000000000000000000000000000..52869ba48ad0234a4359b8858e4654f681870488 --- /dev/null +++ b/dns.go @@ -0,0 +1,46 @@ +package traceroute + +import ( + "errors" + "net" +) + +func resolveDNS(hostname string) (*address, error) { + + ips, err := net.LookupIP(hostname) + if err != nil { + return nil, err + } + + var ipV4 net.IP + for _, ip := range ips { + if len(ip) == net.IPv6len { + return &address{ip: ip, version: addressV6}, nil + } else if len(ip) == net.IPv4len { + ipV4 = ip + } + } + + if ipV4 != nil { + return &address{ip: ipV4, version: addressV4}, nil + } + + return nil, errors.New("could not find a valid record for " + hostname) + +} + +// DNS resolution utility function (IPv6/IPv4) +func resolve(hostname string) (*address, error) { + ip := net.ParseIP(hostname) + + if ip.To4() != nil { + return &address{ip: ip, version: addressV4}, nil + } + + if ip.To16() != nil { + return &address{ip: ip, version: addressV6}, nil + } + + return resolveDNS(hostname) + +} diff --git a/dns_test.go b/dns_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9d35bb648e2e95f4bb8f6cd7c58c93c24a71ee04 --- /dev/null +++ b/dns_test.go @@ -0,0 +1,65 @@ +package traceroute + +import ( + "fmt" + "testing" +) + +func TestResolve(t *testing.T) { + + testCases := []struct { + name string + should string + shouldHasError bool + shouldVersion int + }{ + {"schukai.com", "80.147.206.177", false, addressV4}, + {"unsupported", "", true, addressV4}, + {"::ffff:192.0.2.1", "192.0.2.1", false, addressV4}, // ipv6 masked ipv4 + {"127.0.0.1", "127.0.0.1", false, addressV4}, + {"2001:4860:4860::8888", "2001:4860:4860::8888", false, addressV6}, + {"example.com", "2606:2800:220:1:248:1893:25c8:1946", false, addressV6}, + } + for _, tc := range testCases { + + t.Run(fmt.Sprintf("%s should fail %t", tc.name, tc.shouldHasError), func(t *testing.T) { + address, err := resolve(tc.name) + if err != nil { + if tc.shouldHasError == true { + return + } + t.Errorf("dns query error: %s", err.Error()) + } + + if address.String() != tc.should { + t.Errorf("query %s should %s and not %s", tc.name, tc.should, address.String()) + } + + if tc.shouldVersion == addressV6 { + if address.isV6() != true || address.isV4() != false { + t.Errorf("query %s should version V6", tc.name) + } + } else { + if address.isV6() == true || address.isV4() == false { + t.Errorf("query %s should version V4", tc.name) + } + } + + + }) + + } + +} + +func benchmarkResolve(address string, b *testing.B) { + for n := 0; n < b.N; n++ { + resolve(address) + } +} + +func BenchmarkResolve1(b *testing.B) { benchmarkResolve("www.google.com", b) } +func BenchmarkResolve2(b *testing.B) { benchmarkResolve("www.schukai.com", b) } +func BenchmarkResolve3(b *testing.B) { benchmarkResolve("2001:4860:4860::8888", b) } +func BenchmarkResolve4(b *testing.B) { benchmarkResolve("unsupported", b) } +func BenchmarkResolve5(b *testing.B) { benchmarkResolve("127.0.0.1", b) } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..94ecf4885b42f56d210ccd17cf0384b4c5ba1760 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module gitlab.schukai.com/oss/libraries/go/network/traceroute + +go 1.16 + +require ( + github.com/creasty/defaults v1.5.1 + github.com/jackpal/gateway v1.0.7 + golang.org/x/net v0.0.0-20210510120150-4163338589ed +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..ea2dc19104755a5ea5f53b1198ba334d96e71c1c --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/creasty/defaults v1.5.1 h1:j8WexcS3d/t4ZmllX4GEkl4wIB/trOr035ajcLHCISM= +github.com/creasty/defaults v1.5.1/go.mod h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqLPvXUZGfY= +github.com/jackpal/gateway v1.0.7 h1:7tIFeCGmpyrMx9qvT0EgYUi7cxVW48a0mMvnIL17bPM= +github.com/jackpal/gateway v1.0.7/go.mod h1:aRcO0UFKt+MgIZmRmvOmnejdDT4Y1DNiNOsSd1AcIbA= +golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/ip.go b/ip.go new file mode 100644 index 0000000000000000000000000000000000000000..1adf99da6f5e76dd8e354c4208edf128b96a7389 --- /dev/null +++ b/ip.go @@ -0,0 +1,69 @@ +package traceroute + +import "net" + +const ( + unknown = 0 + addressV4 = 4 + addressV6 = 6 +) + +func isV4(ip net.IP) bool { + i := ip.To4() + if i != nil { + return true + } + return false +} + +type address struct { + ip net.IP + version int +} + +func (a address) isV4() bool { + return a.version == addressV4 +} +func (a address) isV6() bool { + return a.version == addressV6 +} + +func (a address) isUnknow() bool { + return a.version == unknown +} + +func (a address) String() string { + return a.ip.String() +} + +func (a address) Network() string { + r:= net.IPAddr{ + IP: a.ip, + Zone: "", + } + return r.Network() +} + +func newAddress(ip net.IP) address { + + if ip == nil { + return address{ + ip: nil, + version: unknown, + } + + } + + if isV4(ip) { + return address{ + ip: ip, + version: addressV4, + } + } + + return address{ + ip: ip, + version: addressV6, + } + +} diff --git a/ip_test.go b/ip_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a51694762e4d387e8a23e03d26db195486998753 --- /dev/null +++ b/ip_test.go @@ -0,0 +1,38 @@ +package traceroute + +import ( + "fmt" + "net" + "testing" +) + +func TestNewAddress(t *testing.T) { + + testCases := []struct { + name string + mode int + }{ + {"127.0.0.1",addressV4}, + {"unsupported", unknown}, + {"::ffff:192.0.2.1", addressV4}, + {"2001:4860:4860::8888", addressV6}, + {"2606:2800:220:1:248:1893:25c8:1946", addressV6}, + } + for _, tc := range testCases { + + t.Run(fmt.Sprintf("%s should fail %d", tc.name, tc.mode), func(t *testing.T) { + a:= newAddress(net.ParseIP(tc.name)) + + if a.isV4()&&tc.mode!=addressV4 { + t.Errorf("an error has occurred") + } else if a.isV6()&&tc.mode!=addressV6 { + t.Errorf("an error has occurred") + } else if a.isUnknow() &&tc.mode!=unknown{ + t.Errorf("an error has occurred") + } + + }) + + } + +} diff --git a/outbound.go b/outbound.go new file mode 100644 index 0000000000000000000000000000000000000000..764b71c659832eba52d87370bccbf0b02769eb13 --- /dev/null +++ b/outbound.go @@ -0,0 +1,97 @@ +package traceroute + +import ( + "errors" + "github.com/jackpal/gateway" + "net" +) + +func getOutboundIP(mode int) (*address, error) { + ip, err := gateway.DiscoverInterface() + if err != nil { + return nil, err + } + + a := newAddress(ip) + + switch mode { + case addressV4: + if isV4(ip) { + return &a, nil + } + case addressV6: + if !isV4(ip) { + return &a, nil + } + case unknown: + default: + return nil, errors.New("the mode parameter must be either 4 or 6") + } + + interfaces, err := net.Interfaces() + + if err != nil { + return nil, err + } + + for _, i := range interfaces { + addrs, err := i.Addrs() + + if err != nil { + continue + } + + var ipv6, ipv4 net.IP + var found bool + + for _, a := range addrs { + if a.Network() != "ip+net" { + continue + } + + b, _, err := net.ParseCIDR(a.String()) + if err != nil { + continue + } + + if !b.IsGlobalUnicast() { + continue + } + + if isV4(b) { + ipv4 = b + } else { + ipv6 = b + } + + if b.String() == ip.String() { + found = true + } + + } + + if !found { + continue + } + + if mode <= addressV4 { + if ipv4 == nil { + return nil, errors.New("no suitable interface was found") + } + + a:= newAddress(ipv4) + return &a, nil + } + + if ipv6 == nil { + return nil, errors.New("no suitable interface was found") + } + + a:= newAddress(ipv6) + return &a, nil + + } + + return nil, errors.New("no suitable interface was found") + +} diff --git a/outbound_test.go b/outbound_test.go new file mode 100644 index 0000000000000000000000000000000000000000..64587a8508e1b4d99845b009f4933b45df0bdd0e --- /dev/null +++ b/outbound_test.go @@ -0,0 +1,38 @@ +package traceroute + +import ( + "testing" +) + +func TestGetOutboundIPV6(t *testing.T) { + ip, err := getOutboundIP(addressV6) + + if err != nil { + t.Errorf("this call should not return an error " + err.Error()) + } + + if !ip.isV6() { + t.Errorf("this call should not return an error " + err.Error()) + } + + +} + +func TestGetOutboundIPV4(t *testing.T) { + ip, err := getOutboundIP(addressV4) + + if err != nil { + t.Errorf("this call should not return an error " + err.Error()) + } + + if !ip.isV4() { + t.Errorf("this call should not return an error " + err.Error()) + } + +} + +func BenchmarkGetOutboundIP(b *testing.B) { + for n := 0; n < b.N; n++ { + getOutboundIP(addressV6) + } +} diff --git a/session.go b/session.go new file mode 100644 index 0000000000000000000000000000000000000000..b4a51f04226375e02c227dfc0358466541335406 --- /dev/null +++ b/session.go @@ -0,0 +1,59 @@ +package traceroute + +import ( + "github.com/creasty/defaults" + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" + "time" +) + +// Session is Transaction Struct +type Session struct { + Destination *address + Source *address + + CallBack func(result Result) + + Timeout time.Duration `default:"2"` // in seconds + MaxHops int `default:"30"` + + nextHop int + isFinale bool + + ipV4Sock *ipv4.PacketConn + icmpEcho icmp.Message + readBuffer []byte +} + +// NewSession creates a new Session +func NewSession(destination string) (*Session, error) { + + s := Session{} + + if err := defaults.Set(&s); err != nil { + return nil, err + } + + dest, err := resolve(destination) + if err != nil { + return nil, err + } + s.Destination = dest + + var mode int + if dest.isV6() { + mode = addressV6 + } else { + mode = addressV4 + } + + + s.Timeout = s.Timeout*time.Second + //* time.Second + + src, err := getOutboundIP(mode) + s.Source = src + + return &s, nil + +} diff --git a/session_test.go b/session_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e9c8b7a739f491a02f37aad2235478201e37b505 --- /dev/null +++ b/session_test.go @@ -0,0 +1,48 @@ +package traceroute + +import ( + "fmt" + "testing" +) + +func TestNewSession(t *testing.T) { + + testCases := []struct { + destination string + shouldSessionError bool + }{ + { + "www.schukai.com", + false, + }, + } + + for _, tc := range testCases { + + t.Run(fmt.Sprintf(""), func(t *testing.T) { + + session, err := NewSession(tc.destination) + if err != nil { + if tc.shouldSessionError { + return + } + t.Errorf("%s shout not error %s", tc.destination, err.Error()) + return + } + + if session.Timeout==0 { + t.Errorf("timeout should be greater than 0") + } + + _, err = session.TraceRoute() + if err!=nil { + t.Errorf("%s shout not error %s", tc.destination, err.Error()) + return + } + + + }) + + } + +} diff --git a/traceroute.coverprofile b/traceroute.coverprofile new file mode 100644 index 0000000000000000000000000000000000000000..2f575eff08efa23cfe98a890e39ee4c7a39814b7 --- /dev/null +++ b/traceroute.coverprofile @@ -0,0 +1,106 @@ +mode: count +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:26.39,36.16 4 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:41.2,41.45 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:46.2,55.68 4 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:60.2,60.78 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:65.2,67.20 2 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:71.2,71.16 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:76.2,78.16 2 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:83.2,86.50 3 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:91.2,91.47 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:96.2,98.10 2 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:36.16,39.3 2 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:41.45,44.3 2 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:55.68,58.3 2 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:60.78,63.3 2 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:67.20,69.3 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:71.16,74.3 2 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:78.16,81.3 2 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:86.50,89.3 2 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:91.47,94.3 2 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:101.52,105.16 2 2 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:109.2,114.118 4 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:118.2,126.33 4 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:133.2,133.22 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:105.16,107.3 1 2 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:114.118,116.3 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:126.33,128.17 2 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:128.17,129.9 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:142.50,144.26 1 2 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:159.2,159.74 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:144.26,146.17 2 2 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:149.3,149.22 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/traceroute.go:146.17,148.4 1 2 +gitlab.schukai.com/oss/libraries/go/network/traceroute/dns.go:8.52,11.16 2 5 +gitlab.schukai.com/oss/libraries/go/network/traceroute/dns.go:15.2,16.25 2 4 +gitlab.schukai.com/oss/libraries/go/network/traceroute/dns.go:24.2,24.17 1 3 +gitlab.schukai.com/oss/libraries/go/network/traceroute/dns.go:28.2,28.73 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/dns.go:11.16,13.3 1 1 +gitlab.schukai.com/oss/libraries/go/network/traceroute/dns.go:16.25,17.29 1 4 +gitlab.schukai.com/oss/libraries/go/network/traceroute/dns.go:17.29,19.4 1 1 +gitlab.schukai.com/oss/libraries/go/network/traceroute/dns.go:19.9,19.36 1 3 +gitlab.schukai.com/oss/libraries/go/network/traceroute/dns.go:19.36,21.4 1 3 +gitlab.schukai.com/oss/libraries/go/network/traceroute/dns.go:24.17,26.3 1 3 +gitlab.schukai.com/oss/libraries/go/network/traceroute/dns.go:33.49,36.21 2 8 +gitlab.schukai.com/oss/libraries/go/network/traceroute/dns.go:40.2,40.22 1 6 +gitlab.schukai.com/oss/libraries/go/network/traceroute/dns.go:44.2,44.29 1 5 +gitlab.schukai.com/oss/libraries/go/network/traceroute/dns.go:36.21,38.3 1 2 +gitlab.schukai.com/oss/libraries/go/network/traceroute/dns.go:40.22,42.3 1 1 +gitlab.schukai.com/oss/libraries/go/network/traceroute/ip.go:11.27,13.14 2 15 +gitlab.schukai.com/oss/libraries/go/network/traceroute/ip.go:16.2,16.14 1 4 +gitlab.schukai.com/oss/libraries/go/network/traceroute/ip.go:13.14,15.3 1 11 +gitlab.schukai.com/oss/libraries/go/network/traceroute/ip.go:24.30,26.2 1 13 +gitlab.schukai.com/oss/libraries/go/network/traceroute/ip.go:27.30,29.2 1 13 +gitlab.schukai.com/oss/libraries/go/network/traceroute/ip.go:31.34,33.2 1 5 +gitlab.schukai.com/oss/libraries/go/network/traceroute/ip.go:35.34,37.2 1 5 +gitlab.schukai.com/oss/libraries/go/network/traceroute/ip.go:39.36,45.2 2 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/ip.go:47.36,49.15 1 10 +gitlab.schukai.com/oss/libraries/go/network/traceroute/ip.go:57.2,57.14 1 9 +gitlab.schukai.com/oss/libraries/go/network/traceroute/ip.go:64.2,67.3 1 3 +gitlab.schukai.com/oss/libraries/go/network/traceroute/ip.go:49.15,55.3 1 1 +gitlab.schukai.com/oss/libraries/go/network/traceroute/ip.go:57.14,62.3 1 6 +gitlab.schukai.com/oss/libraries/go/network/traceroute/main.go:3.14,5.2 0 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:9.48,11.16 2 4 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:15.2,17.14 2 4 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:31.2,33.16 2 1 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:37.2,37.31 1 1 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:95.2,95.59 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:11.16,13.3 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:18.17,19.15 1 3 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:22.17,23.16 1 1 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:26.15,26.15 0 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:27.10,28.69 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:19.15,21.4 1 3 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:23.16,25.4 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:33.16,35.3 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:37.31,40.17 2 2 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:44.3,47.27 3 2 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:73.3,73.13 1 2 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:77.3,77.24 1 1 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:86.3,86.18 1 1 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:90.3,91.17 2 1 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:40.17,41.12 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:47.27,48.31 1 5 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:52.4,53.18 2 5 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:57.4,57.28 1 5 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:61.4,61.15 1 2 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:67.4,67.33 1 2 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:48.31,49.13 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:53.18,54.13 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:57.28,58.13 1 3 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:61.15,63.5 1 1 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:63.10,65.5 1 1 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:67.33,69.5 1 1 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:73.13,74.12 1 1 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:77.24,78.19 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:82.4,83.18 2 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:78.19,80.5 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/outbound.go:86.18,88.4 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/session.go:27.55,31.41 2 2 +gitlab.schukai.com/oss/libraries/go/network/traceroute/session.go:35.2,36.16 2 2 +gitlab.schukai.com/oss/libraries/go/network/traceroute/session.go:39.2,42.17 3 2 +gitlab.schukai.com/oss/libraries/go/network/traceroute/session.go:49.2,55.16 4 2 +gitlab.schukai.com/oss/libraries/go/network/traceroute/session.go:31.41,33.3 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/session.go:36.16,38.3 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/session.go:42.17,44.3 1 0 +gitlab.schukai.com/oss/libraries/go/network/traceroute/session.go:44.8,46.3 1 2 diff --git a/traceroute.go b/traceroute.go new file mode 100644 index 0000000000000000000000000000000000000000..c867c04e83f4f748cf0b85cb0c1182085e174c32 --- /dev/null +++ b/traceroute.go @@ -0,0 +1,166 @@ +package traceroute + +import ( + "errors" + "fmt" + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" + "math/rand" + "net" + + "time" +) + +type Result struct { + Hop int + Station string + Latency time.Duration + Err error +} + +// Results is a collection of hops +type Results struct { + Hops []Result +} + +func (s *Session) doHop(i int) Result { + s.icmpEcho.Body.(*icmp.Echo).Seq = i + + r := Result{ + Hop: i, + Station: "*", + } + + writeBuffer, err := s.icmpEcho.Marshal(nil) + + if err != nil { + r.Err = err + return r + } + + if err := s.ipV4Sock.SetTTL(i); err != nil { + r.Err = fmt.Errorf("socket: %w", err) + return r + } + + timeNow := time.Now() + + dst := s.Destination + + a := net.IPAddr{ + IP: dst.ip, + Zone: "", + } + + if _, err := s.ipV4Sock.WriteTo(writeBuffer, nil, &a); err != nil { + r.Err = err + return r + } + + if err := s.ipV4Sock.SetReadDeadline(time.Now().Add(s.Timeout)); err != nil { + r.Err = err + return r + } + + readBytes, _, hopNode, err := s.ipV4Sock.ReadFrom(s.readBuffer) + + if hopNode != nil { + r.Station = hopNode.String() + } + + if err != nil { + r.Err = err + return r + } + + icmpAnswer, err := icmp.ParseMessage(1, s.readBuffer[:readBytes]) + + if err != nil { + r.Err = err + return r + } + + latency := time.Since(timeNow) + r.Latency = latency + + if icmpAnswer.Type == ipv4.ICMPTypeTimeExceeded { + s.nextHop++ + return r + } + + if icmpAnswer.Type == ipv4.ICMPTypeEchoReply { + s.isFinale = true + return r + } + + r.Err = fmt.Errorf("unknown icmp answer: %d", icmpAnswer.Type.Protocol()) + + return r +} + +func (s *Session) TraceRouteV4() (*Results, error) { + + sock, err := net.ListenPacket("ip4:icmp", s.Source.ip.String()) + + if err != nil { + return nil, err + } + + defer sock.Close() + + s.ipV4Sock = ipv4.NewPacketConn(sock) + defer s.ipV4Sock.Close() + + if err := s.ipV4Sock.SetControlMessage(ipv4.FlagTTL|ipv4.FlagDst|ipv4.FlagInterface|ipv4.FlagSrc, true); err != nil { + return nil, err + } + + s.icmpEcho = icmp.Message{ + Type: ipv4.ICMPTypeEcho, Code: 0, Body: &icmp.Echo{ID: rand.Int(), Data: []byte("")}, + } + + s.readBuffer = make([]byte, 1500) + + results := Results{} + + for i := 1; i < s.MaxHops; i++ { + r:=s.doHop(i) + results.Hops = append(results.Hops, r) + if s.CallBack!=nil { + s.CallBack(r) + } + + if s.isFinale { + break + } + } + + return &results, nil +} + +// currently not implemented +//func (s *Session) traceRouteV6() error { +// return nil +//} + +// TraceRoute measures the steps to the target host +func (s *Session) TraceRoute() (*Results, error) { + + if s.Destination.isV4() { + results, err := s.TraceRouteV4() + if err != nil { + return nil, err + } + return results, nil + } + + // currently not implemented + //if s.Destination.isV6() { + // if err := s.traceRouteV6(); err != nil { + // return nil, err + // } + //} + + return nil, errors.New("could not traceroute " + s.Destination.String()) + +} diff --git a/traceroute.iml b/traceroute.iml new file mode 100644 index 0000000000000000000000000000000000000000..eacc75a3ca037d63f5c1eafd27c21c99c66161a5 --- /dev/null +++ b/traceroute.iml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<module type="WEB_MODULE" version="4"> + <component name="Go" enabled="true" /> + <component name="NewModuleRootManager" inherit-compiler-output="true"> + <exclude-output /> + <content url="file://$MODULE_DIR$" /> + <orderEntry type="sourceFolder" forTests="false" /> + </component> +</module> \ No newline at end of file diff --git a/traceroute_test.go b/traceroute_test.go new file mode 100644 index 0000000000000000000000000000000000000000..13931d419ac9253e3c91e0a99ce0f67526f79834 --- /dev/null +++ b/traceroute_test.go @@ -0,0 +1,46 @@ +package traceroute + +import ( + "fmt" + "testing" +) + +func TestTraceroute(t *testing.T) { + + testCases := []struct { + destination string + shouldSessionError bool + }{ + { + "www.schukai.com", + false, + }, + } + + for _, tc := range testCases { + + t.Run(fmt.Sprintf(""), func(t *testing.T) { + + session, err := NewSession(tc.destination) + if err != nil { + t.Errorf("%s shout not error %s", tc.destination, err.Error()) + } + + _,err = session.TraceRoute() + if err != nil { + t.Errorf("%s shout not error %s", tc.destination, err.Error()) + } + + }) + + } + +} + +func benchmarkTraceroute(address string, b *testing.B) { + for n := 0; n < b.N; n++ { + resolve(address) + } +} + +func BenchmarkTraceroute1(b *testing.B) { benchmarkTraceroute("www.google.com", b) }