diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..8b6694c --- /dev/null +++ b/.drone.yml @@ -0,0 +1,35 @@ +kind: pipeline +type: docker +name: default + +trigger: + branch: + - master + +steps: + # Unit Tests + - name: tests + image: golang + commands: + - make tests + + # Fetch tags + - name: fetch tags + image: alpine/git + commands: + - git fetch --tags + + # Publish docker image + - name: publish docker + image: plugins/docker + settings: + repo: gitea.va.reichard.io/evan/conduit + registry: gitea.va.reichard.io + tags: + - dev + custom_dns: + - 8.8.8.8 + username: + from_secret: docker_username + password: + from_secret: docker_password diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d393d4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +cover.html diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8820789 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +# Certificates & Timezones +FROM alpine AS alpine +RUN apk update && apk add --no-cache ca-certificates tzdata + +# Build Image +FROM golang:1.24 AS build + +# Create Package Directory +RUN mkdir -p /opt/conduit + +# Copy Source +WORKDIR /src +COPY . . + +# Compile +RUN go build \ + -ldflags "-X reichard.io/conduit/config.version=`git describe --tags`" \ + -o /opt/conduit/conduit + +# Create Image +FROM busybox:1.36 +COPY --from=alpine /etc/ssl/certs /etc/ssl/certs +COPY --from=alpine /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=build /opt/conduit /opt/conduit +WORKDIR /opt/conduit +EXPOSE 8080 +ENTRYPOINT ["/opt/conduit/conduit", "serve"] diff --git a/Dockerfile-BuildKit b/Dockerfile-BuildKit new file mode 100644 index 0000000..67914bb --- /dev/null +++ b/Dockerfile-BuildKit @@ -0,0 +1,29 @@ +# Certificates & Timezones +FROM alpine AS alpine +RUN apk update && apk add --no-cache ca-certificates tzdata + +# Build Image +FROM --platform=$BUILDPLATFORM golang:1.24 AS build + +# Create Package Directory +WORKDIR /src +RUN mkdir -p /opt/conduit + +# Cache Dependencies & Compile +ARG TARGETOS +ARG TARGETARCH +RUN --mount=target=. \ + --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + GOOS=$TARGETOS GOARCH=$TARGETARCH go build \ + -ldflags "-X reichard.io/conduit/config.version=`git describe --tags`" \ + -o /opt/conduit/conduit + +# Create Image +FROM busybox:1.36 +COPY --from=alpine /etc/ssl/certs /etc/ssl/certs +COPY --from=alpine /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=build /opt/conduit /opt/conduit +WORKDIR /opt/conduit +EXPOSE 8080 +ENTRYPOINT ["/opt/conduit/conduit", "serve"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..191502d --- /dev/null +++ b/Makefile @@ -0,0 +1,35 @@ +build_local: + go mod download + rm -r ./build || true + mkdir -p ./build + + env GOOS=linux GOARCH=amd64 go build -ldflags "-X reichard.io/conduit/config.version=`git describe --tags`" -o ./build/server_linux_amd64 + env GOOS=linux GOARCH=arm64 go build -ldflags "-X reichard.io/conduit/config.version=`git describe --tags`" -o ./build/server_linux_arm64 + env GOOS=darwin GOARCH=arm64 go build -ldflags "-X reichard.io/conduit/config.version=`git describe --tags`" -o ./build/server_darwin_arm64 + env GOOS=darwin GOARCH=amd64 go build -ldflags "-X reichard.io/conduit/config.version=`git describe --tags`" -o ./build/server_darwin_amd64 + +docker_build_local: + docker build -t conduit:latest . + +docker_build_release_dev: + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t gitea.va.reichard.io/evan/conduit:dev \ + -f Dockerfile-BuildKit \ + --push . + +docker_build_release_latest: + docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t gitea.va.reichard.io/evan/conduit:latest \ + -t gitea.va.reichard.io/evan/conduit:`git describe --tags` \ + -f Dockerfile-BuildKit \ + --push . + +clean: + rm -rf ./build + +tests: + SET_TEST=set_val go test -coverpkg=./... ./... -coverprofile=./cover.out + go tool cover -html=./cover.out -o ./cover.html + rm ./cover.out diff --git a/cmd/tunnel.go b/cmd/tunnel.go new file mode 100644 index 0000000..73b64e6 --- /dev/null +++ b/cmd/tunnel.go @@ -0,0 +1,39 @@ +package cmd + +import ( + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "reichard.io/conduit/client" + "reichard.io/conduit/config" +) + +var tunnelCmd = &cobra.Command{ + Use: "tunnel ", + Short: "Create a conduit tunnel", + Run: func(cmd *cobra.Command, args []string) { + // Get Client Config + cfg, err := config.GetClientConfig(cmd.Flags()) + if err != nil { + log.Fatal("failed to get client config:", err) + } + + // Create Tunnel + tunnel, err := client.NewTunnel(cfg) + if err != nil { + log.Fatal("failed to create tunnel:", err) + } + + // Start Tunnel + log.Infof("creating TCP tunnel: %s -> %s", cfg.TunnelName, cfg.TunnelTarget) + if err := tunnel.Start(); err != nil { + log.Fatal("failed to start tunnel:", err) + } + }, +} + +func init() { + configDefs := config.GetConfigDefs[config.ClientConfig]() + for _, d := range configDefs { + tunnelCmd.Flags().String(d.Key, d.Default, d.Description) + } +} diff --git a/config/config.go b/config/config.go index 58e6abf..1d0c4e6 100644 --- a/config/config.go +++ b/config/config.go @@ -11,6 +11,8 @@ import ( "github.com/spf13/pflag" ) +var version string = "develop" + type ConfigDef struct { Key string Env string @@ -102,6 +104,10 @@ func GetConfigDefs[T ServerConfig | ClientConfig]() []ConfigDef { return defs } +func GetVersion() string { + return version +} + func getConfigValue(cmdFlags *pflag.FlagSet, def ConfigDef) string { // 1. Get Flags First if cmdFlags != nil { diff --git a/server/server.go b/server/server.go index aa4dd24..197ef4a 100644 --- a/server/server.go +++ b/server/server.go @@ -3,6 +3,7 @@ package server import ( "bufio" "bytes" + "encoding/json" "errors" "fmt" "io" @@ -19,6 +20,16 @@ import ( "reichard.io/conduit/types" ) +type InfoResponse struct { + Tunnels []TunnelInfo `json:"tunnels"` + Version string `json:"version"` +} + +type TunnelInfo struct { + Name string `json:"name"` + Target string `json:"target"` +} + type TunnelConnection struct { *websocket.Conn name string @@ -76,16 +87,31 @@ func (s *Server) Start() error { } } -func (s *Server) getStatus(w http.ResponseWriter, _ *http.Request) { +func (s *Server) getInfo(w http.ResponseWriter, _ *http.Request) { + // Get Tunnels + var allTunnels []TunnelInfo s.mu.RLock() - count := len(s.tunnels) + for t, c := range s.tunnels { + allTunnels = append(allTunnels, TunnelInfo{ + Name: t, + Target: c.RemoteAddr().String(), + }) + } s.mu.RUnlock() - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) + // Create Response + d, err := json.MarshalIndent(InfoResponse{ + Tunnels: allTunnels, + Version: config.GetVersion(), + }, "", " ") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } - response := fmt.Sprintf(`{"tunnels": %d}`, count) - _, _ = w.Write([]byte(response)) + // Send Response + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(d) } func (s *Server) proxyRawConnection(clientConn net.Conn, tunnelConn *TunnelConnection, dataReader io.Reader) { @@ -212,8 +238,8 @@ func (s *Server) handleAsHTTP(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/_conduit/tunnel": s.createTunnel(w, r) - case "/_conduit/status": - s.getStatus(w, r) + case "/_conduit/info": + s.getInfo(w, r) default: w.WriteHeader(http.StatusNotFound) }