Skip to content

Commit

Permalink
Intial version
Browse files Browse the repository at this point in the history
  • Loading branch information
everesio committed Feb 26, 2024
1 parent c28efdb commit 44a9428
Show file tree
Hide file tree
Showing 82 changed files with 5,733 additions and 10 deletions.
65 changes: 56 additions & 9 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,21 +1,68 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
/bin
/dist
/target
/.cr-release-packages
/vendor
/reverse-http
/*.pem
/*.b64

/*.tar
/*.tgz

# Intellij
.idea/
out/
*.iml

# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
# Test binary, build with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o

# Folders
_obj
_test

# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out

*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*

_testmain.go

*.prof

# coverage
.coverprofile
gover.coverprofile

# Swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-v][a-z]
[._]sw[a-p]

# Session
Session.vim

# Go workspace file
go.work
# Temporary
.netrwhist
*~
# Auto-generated tag files
tags
34 changes: 34 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# options for analysis running
run:
# exit code when at least one issue was found, default is 1
issues-exit-code: 1

# which dirs to skip: they won't be analyzed;
# can use regexp here: generated.*, regexp is applied on full path;
# default value is empty list, but next dirs are always skipped independently
# from this option's value:
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
skip-dirs:
- vendor

linters:
enable:
- errcheck
- goconst
- godot
- gofmt
- goimports
- gosimple
- govet
- ineffassign
- staticcheck
- typecheck
- unparam
- unused
- exportloopref

issues:
exclude-rules:
- path: _test\.go
linters:
- unparam
16 changes: 16 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM golang:1.21-alpine3.19 AS builder
# hadolint ignore=DL3018
RUN apk add --no-cache alpine-sdk ca-certificates curl

WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN make vendor build

FROM alpine:3.19

COPY --from=builder /app/reverse-http /reverse-http

USER 65532:65532
ENTRYPOINT ["/reverse-http"]
117 changes: 117 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
.DEFAULT_GOAL := help

.PHONY: clean build fmt test

ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))

BUILD_FLAGS ?=
VERSION = "0.0.1"
BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
REVISION = $(shell git describe --tags --always --dirty)
BUILD_DATE = $(shell date +'%Y.%m.%d-%H:%M:%S')
LDFLAGS ?= -w -s
BINARY = reverse-http

TEST_AGENT_ID = 4711
TEST_AUTH = ha-tls
TEST_STORE_TYPE = none

default: help

.PHONY: help
help:
@grep -E '^[a-zA-Z%_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

build: ## Build executable
@CGO_ENABLED=0 GO111MODULE=on go build -mod=vendor -o $(BINARY) $(BUILD_FLAGS) -ldflags "$(LDFLAGS)" .

test: ## Test
@GO111MODULE=on go test -count=1 -mod=vendor -v ./...

fmt: ## Go format
go fmt ./...

vet: ## Go vet
go vet ./...

clean: ## Clean
@rm -rf $(BINARY)

lint: ## Lint
@golangci-lint run

.PHONY: deps
deps: ## Get dependencies
GO111MODULE=on go get ./...

.PHONY: vendor
vendor: ## Go vendor
GO111MODULE=on go mod vendor

.PHONY: tidy
tidy: ## Go tidy
GO111MODULE=on go mod tidy

##### Testing

docker-compose.build:
docker-compose -f $(ROOT_DIR)/docker-compose.${TEST_AUTH}.yml build

docker-compose.up:
docker-compose -f $(ROOT_DIR)/docker-compose.${TEST_AUTH}.yml up --remove-orphans

docker-compose.down:
docker-compose -f $(ROOT_DIR)/docker-compose.${TEST_AUTH}.yml down --remove-orphans

docker-compose.run: docker-compose.build docker-compose.up

start-proxy: build
@export QUIC_GO_LOG_LEVEL_=debug && ${ROOT_DIR}/reverse-http proxy --store.type="${TEST_STORE_TYPE}" --agent-server.listen-address=":4242" \
--http-proxy.listen-address=":3128" --agent-server.tls.file.key=tests/cfssl/certs/proxy-key.pem --agent-server.tls.file.cert=tests/cfssl/certs/proxy.pem

start-proxy-tls: build
@export QUIC_GO_LOG_LEVEL_=debug && ${ROOT_DIR}/reverse-http proxy --store.type="${TEST_STORE_TYPE}" --agent-server.listen-address=":4242" \
--http-proxy.listen-address=":3128" --agent-server.tls.file.key=tests/cfssl/certs/proxy-key.pem --agent-server.tls.file.cert=tests/cfssl/certs/proxy.pem \
--http-proxy.tls.enable --http-proxy.tls.file.key=tests/cfssl/certs/proxy-key.pem --http-proxy.tls.file.cert=tests/cfssl/certs/proxy.pem

start-proxy2: build
@${ROOT_DIR}/reverse-http proxy --store.type="memcached" --agent-server.listen-address=":4243" --http-proxy.listen-address=":3127" \
--store.http-proxy-address="localhost:3127" --agent-server.tls.file.key=tests/cfssl/certs/proxy-key.pem --agent-server.tls.file.cert=tests/cfssl/certs/proxy.pem

start-agent: build
@export QUIC_GO_LOG_LEVEL_=debug && ${ROOT_DIR}/reverse-http agent --auth.noauth.agent-id="4711" --agent-client.server-address="localhost:4242" --agent-client.tls.file.root-ca=tests/cfssl/certs/ca.pem

start-agent2: build
@${ROOT_DIR}/reverse-http agent --auth.noauth.agent-id="4712" --agent-client.server-address="localhost:4243" --agent-client.tls.insecure-skip-verify

start-lb: build
@${ROOT_DIR}/reverse-http lb --http-proxy.listen-address=":3129" --store.type="${TEST_STORE_TYPE}"

curl-proxy:
curl -x "http://${TEST_AGENT_ID}:noauth@localhost:3128" https://httpbin.org/ip

curl-proxy-tls:
curl -x "https://${TEST_AGENT_ID}:noauth@localhost:3128" https://httpbin.org/ip --proxy-cacert tests/cfssl/certs/ca.pem

curl-lb:
curl -x "http://${TEST_AGENT_ID}:noauth@localhost:3129" https://httpbin.org/ip

jwt-keys: build
@${ROOT_DIR}/reverse-http auth key private --out=${ROOT_DIR}/tests/jwt/auth-key-private.pem
@${ROOT_DIR}/reverse-http auth key public --out=${ROOT_DIR}/tests/jwt/auth-key-public.pem --in=${ROOT_DIR}/tests/jwt/auth-key-private.pem
@${ROOT_DIR}/reverse-http auth jwt token --duration=87600h --agent-id="4711" --role "client" --out ${ROOT_DIR}/tests/jwt/auth-client-jwt-4711.b64 --in=${ROOT_DIR}/tests/jwt/auth-key-private.pem
@${ROOT_DIR}/reverse-http auth jwt token --duration=87600h --agent-id="4711" --role "agent" --out ${ROOT_DIR}/tests/jwt/auth-agent-jwt-4711.b64 --in=${ROOT_DIR}/tests/jwt/auth-key-private.pem
@${ROOT_DIR}/reverse-http auth jwt token --duration=87600h --agent-id="4712" --role "client" --out ${ROOT_DIR}/tests/jwt/auth-client-jwt-4712.b64 --in=${ROOT_DIR}/tests/jwt/auth-key-private.pem
@${ROOT_DIR}/reverse-http auth jwt token --duration=87600h --agent-id="4712" --role "agent" --out ${ROOT_DIR}/tests/jwt/auth-agent-jwt-4712.b64 --in=${ROOT_DIR}/tests/jwt/auth-key-private.pem

start-proxy-jwt: build
@${ROOT_DIR}/reverse-http proxy --auth.type="jwt" --auth.jwt.public-key=tests/jwt/auth-key-public.pem \
--agent-server.listen-address=":4242" --http-proxy.listen-address=":3128" --agent-server.tls.file.key=tests/cfssl/certs/proxy-key.pem --agent-server.tls.file.cert=tests/cfssl/certs/proxy.pem

start-agent-jwt: build
@$(eval JWT_TOKEN=$(shell cat tests/jwt/auth-agent-jwt-${TEST_AGENT_ID}.b64))
@${ROOT_DIR}/reverse-http agent --auth.type="jwt" --agent-client.server-address="localhost:4242" --agent-client.tls.file.root-ca=tests/cfssl/certs/ca.pem --auth.jwt.token="file:tests/jwt/auth-agent-jwt-${TEST_AGENT_ID}.b64"

curl-proxy-jwt:
@$(eval JWT_TOKEN=$(shell cat tests/jwt/auth-client-jwt-${TEST_AGENT_ID}.b64))
curl -x "http://${TEST_AGENT_ID}:${JWT_TOKEN}@localhost:3128" https://httpbin.org/ip
107 changes: 106 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,107 @@
# reverse-http
Reverse HTTP proxy over QUIC protocol

Reverse HTTP proxy over QUIC protocol ([RFC 9000](https://datatracker.ietf.org/doc/html/rfc9000)).

## Architecture

### Standalone

<p style="text-align: center;"><img src="docs/reverse-http-arch.svg" alt="Architecture"></p>

* Agent connection process
* An agent initiates a connection to the proxy server utilizing the `QUIC` protocol.
* The connection between the agent and the proxy is persistent
* Upon connection, the proxy server performs an agent authentication
* The proxy keeps track of agents' connections
* Each agent is uniquely identified by an `agentID`
* Multiple agents can simultaneously connect to the proxy.
* Only one connection per `agentID` is allowed.

* Client connection process
* Clients establish a connection with the HTTP proxy by issuing an `HTTP CONNECT` request. This standard method allows the client to specify the desired destination.
* During the connection process, the proxy authenticates the connecting client using basic `Proxy-Authorization`, where the `username` is utilized to specify the `agentID` that the client wishes to connect to.
* Once authenticated, the proxy server locates the corresponding agent's `QUIC` connection that is already being tracked.
* Proxy opens a new `QUIC` stream to the agent and sends all subsequent data through it
* The agent proceeds with the `CONNECT` procedure by establishing a new TCP connection to the requested destination.

### HA setup

<p style="text-align: center;"><img src="docs/reverse-http-ha.svg" alt="HA"></p>

* Agent connection process
* An agent initiates a connection to the UDP load balancer, which in turn establishes a connection with one of the proxy servers
* Upon establishing a connection, the proxy server records an entry in `memcached` for an agentID along with its own HTTP proxy address.
* Client connection process
* Clients connect to the TCP load balancer, which then establishes a connection with one of the LB servers.
* Upon connection, the LB server retrieves the HTTP proxy address and an agentID from Memcached.
* The LB server then sends an `HTTP CONNECT` request to the proxy.

## Build
### build binary

make clean build

## Quick requirements

https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes

```bash
sudo bash -c 'echo net.core.rmem_max=2500000 >> /etc/sysctl.conf'
sudo bash -c 'echo net.core.wmem_max=2500000 >> /etc/sysctl.conf'
sudo sysctl -p
```



## Local test standalone

### no auth

```bash
make start-proxy
make start-agent
curl -x "http://4711:noauth@localhost:3128" https://httpbin.org/ip
```

### jwt auth

```bash
make start-proxy-jwt
make start-agent-jwt
make curl-proxy-jwt
```

## Local test docker-compose

```bash
make TEST_AUTH=noauth docker-compose.run
make TEST_AGENT_ID=4711 curl-proxy
make TEST_AGENT_ID=4712 curl-proxy
```

## Whitelisting patterns

```
localhost
localhost:80
localhost:1000-2000
*.zone
*.zone:80
*.zone:1000-2000
127.0.0.1
127.0.0.1:80
127.0.0.1:1000-2000
10.0.0.1/8
10.0.0.1/8:80
10.0.0.1/8:1000-2000
1000::/16
1000::/16:80
1000::/16:1000-2000
[2001:db8::1]/64
[2001:db8::1]/64:80
[2001:db8::1]/64:1000-2000
2001:db8::1
[2001:db8::1]
[2001:db8::1]:80
[2001:db8::1]:1000-2000
```
Loading

0 comments on commit 44a9428

Please sign in to comment.