Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Features/change warp to tor #28

Merged
merged 19 commits into from
Dec 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9b88bb6
feat(tor): introduce Tor support for nanoproxy
ryanbekhen Dec 7, 2024
eb04cbc
fix(deps): remove unused extra_files section from goreleaser.yml
ryanbekhen Dec 7, 2024
1ccb0d3
feat: integrate Tor with nanoproxy startup script
ryanbekhen Dec 7, 2024
392c428
build: update Dockerfile to modify entrypoint script
ryanbekhen Dec 7, 2024
70c5723
fix: rename script to entrypoint.sh
ryanbekhen Dec 7, 2024
617ba26
fix(build): rename start script in Dockerfile-tor
ryanbekhen Dec 7, 2024
f27882e
fix: make entrypoint.sh executable in Dockerfile
ryanbekhen Dec 7, 2024
253bd26
fix: improve Tor bootstrap check reliability
ryanbekhen Dec 7, 2024
24adfb0
fix: remove DataDirectory setting from torrc file
ryanbekhen Dec 7, 2024
8c5ce60
feat(tor): integrate supervisor and refactor Tor handling
ryanbekhen Dec 7, 2024
7d9dfd4
fix: remove user tor from Dockerfile-tor
ryanbekhen Dec 7, 2024
98bdde7
feat(tor): add tor bootstrap status check function
ryanbekhen Dec 7, 2024
0fb599f
fix: standardize log messages to English
ryanbekhen Dec 7, 2024
6903b90
feat(tor): simplify identity switch mechanism
ryanbekhen Dec 7, 2024
113b1f6
feat(tor): add new Tor identity management and tests
ryanbekhen Dec 8, 2024
3deb135
fix(tests): simplify Dial function with mock Dialer
ryanbekhen Dec 8, 2024
fe21f24
fix(tests): revise SOCKS5 dialer mocks in tests
ryanbekhen Dec 8, 2024
d4a2931
build: apply build constraint for non-test environments
ryanbekhen Dec 8, 2024
524168d
fix(deps): update test coverage exclusions
ryanbekhen Dec 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ dockers:
goarch: arm64

- image_templates:
- "ghcr.io/ryanbekhen/nanoproxy-warp:{{ .Version }}-amd64"
dockerfile: Dockerfile-warp
- "ghcr.io/ryanbekhen/nanoproxy-tor:{{ .Version }}-amd64"
dockerfile: Dockerfile-tor
build_flag_templates:
- "--label=io.artifacthub.package.readme-url=https://raw.githubusercontent.com/ryanbekhen/nanoproxy/main/README.md"
- '--label=io.artifacthub.package.maintainers=[{"name":"Achmad Irianto Eka Putra","email":"i@ryanbekhen.dev"}]'
Expand All @@ -127,7 +127,7 @@ dockers:
- "--label=org.opencontainers.image.source={{.GitURL}}"
- "--platform=linux/amd64"
extra_files:
- script.sh
- supervisord.conf

docker_manifests:
- name_template: "ghcr.io/ryanbekhen/nanoproxy:{{ .Version }}"
Expand All @@ -138,12 +138,12 @@ docker_manifests:
image_templates:
- "ghcr.io/ryanbekhen/nanoproxy:{{ .Version }}-amd64"
- "ghcr.io/ryanbekhen/nanoproxy:{{ .Version }}-arm64"
- name_template: "ghcr.io/ryanbekhen/nanoproxy-warp:{{ .Version }}"
- name_template: "ghcr.io/ryanbekhen/nanoproxy-tor:{{ .Version }}"
image_templates:
- "ghcr.io/ryanbekhen/nanoproxy-warp:{{ .Version }}-amd64"
- name_template: "ghcr.io/ryanbekhen/nanoproxy-warp:latest"
- "ghcr.io/ryanbekhen/nanoproxy-tor:{{ .Version }}-amd64"
- name_template: "ghcr.io/ryanbekhen/nanoproxy-tor:latest"
image_templates:
- "ghcr.io/ryanbekhen/nanoproxy-warp:{{ .Version }}-amd64"
- "ghcr.io/ryanbekhen/nanoproxy-tor:{{ .Version }}-amd64"

archives:
- name_template: >-
Expand Down
3 changes: 2 additions & 1 deletion .testcoverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ threshold:
total: 80
exclude:
paths:
- \.pb\.go$ # excludes all protobuf generated files
- \.pb\.go$ # excludes all protobuf generated files
- nanoproxy\.go$ # excludes the main package
18 changes: 18 additions & 0 deletions Dockerfile-tor
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
FROM alpine:3.21

RUN apk update && \
apk add --no-cache tor supervisor

RUN mkdir -p /var/log/supervisor

COPY nanoproxy /usr/bin/nanoproxy
COPY supervisord.conf /etc/supervisord.conf

RUN mkdir -p /etc/tor && \
echo -e "ControlPort 9051\nCookieAuthentication 0" > /etc/tor/torrc

RUN mkdir -p /var/lib/tor

EXPOSE 1080

ENTRYPOINT ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
28 changes: 0 additions & 28 deletions Dockerfile-warp

This file was deleted.

37 changes: 19 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,13 @@ Here's how the data flows through the proxy:

```mermaid
sequenceDiagram
participant Network
participant Proxy
participant DestinationServer

Network->>Proxy: Request
Proxy->>DestinationServer: Forward Request
DestinationServer->>Proxy: Process & Respond
Proxy->>Network: Respond
participant Network
participant Proxy
participant DestinationServer
Network ->> Proxy: Request
Proxy ->> DestinationServer: Forward Request
DestinationServer ->> Proxy: Process & Respond
Proxy ->> Network: Respond
```

This clear separation of responsibilities helps optimize network communication and enables various proxy-related
Expand All @@ -45,7 +44,7 @@ NanoProxy provides the following features:

- **SOCKS5 proxy server.** NanoProxy is a SOCKS5 proxy server that can be used to proxy network traffic for various
applications.
- **WARP Cloudflare support.** NanoProxy supports running behind Cloudflare's WARP service (Docker only).
- **TOR support.** NanoProxy can be run with Tor support to provide anonymized network traffic (Docker only).

## Installation

Expand Down Expand Up @@ -124,7 +123,7 @@ docker run -p 1080:1080 ghcr.io/ryanbekhen/nanoproxy:latest
You can also run NanoProxy behind Cloudflare's WARP service using Docker. To do so, you can use the following command:

```shell
docker run --rm -d --privileged --cap-add=NET_ADMIN --sysctl net.ipv6.conf.all.disable_ipv6=0 --sysctl net.ipv4.conf.all.src_valid_mark=1 -p 1080:1080 ghcr.io/ryanbekhen/nanoproxy-warp:latest
docker run --rm -d --privileged --cap-add=NET_ADMIN --sysctl net.ipv6.conf.all.disable_ipv6=0 --sysctl net.ipv4.conf.all.src_valid_mark=1 -p 1080:1080 ghcr.io/ryanbekhen/nanoproxy-tor:latest
```

## Configuration
Expand Down Expand Up @@ -159,14 +158,16 @@ password hash. You can then use the output to set the `CREDENTIALS` environment

The following table lists the available configuration options:

| Name | Description | Default Value |
|----------------|-------------------------------------------------------|---------------|
| ADDR | The address to listen on. | `:1080` |
| NETWORK | The network to listen on. (tcp, tcp4, tcp6) | `tcp` |
| TZ | The timezone to use. | `Local` |
| CLIENT_TIMEOUT | The timeout for connecting to the destination server. | `10s` |
| DNS_TIMEOUT | The timeout for DNS resolution. | `10s` |
| CREDENTIALS | The credentials to use for authentication. | `""` |
| Name | Description | Default Value |
|-----------------------|-----------------------------------------------------------------|---------------|
| ADDR | The address to listen on. | `:1080` |
| NETWORK | The network to listen on. (tcp, tcp4, tcp6) | `tcp` |
| TZ | The timezone to use. | `Local` |
| CLIENT_TIMEOUT | The timeout for connecting to the destination server. | `10s` |
| DNS_TIMEOUT | The timeout for DNS resolution. | `10s` |
| CREDENTIALS | The credentials to use for authentication. | `""` |
| TOR_ENABLED | Enable Tor support. (works only on Docker) | `false` |
| TOR_IDENTITY_INTERVAL | The interval to change the Tor identity. (works only on Docker) | `10m` |ß

## Logging

Expand Down
2 changes: 1 addition & 1 deletion config/nanoproxy
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
ADDR=:1080
NETWORK=tcp
TZ=Local
TZ=Local
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/rs/zerolog v1.33.0
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.30.0
golang.org/x/net v0.32.0
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
17 changes: 17 additions & 0 deletions nanoproxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/rs/zerolog"
"github.com/ryanbekhen/nanoproxy/pkg/config"
"github.com/ryanbekhen/nanoproxy/pkg/socks5"
"github.com/ryanbekhen/nanoproxy/pkg/tor"
"os"
"strings"
"time"
Expand Down Expand Up @@ -43,6 +44,22 @@ func main() {
if len(credentials) > 0 {
socks5Config.Credentials = credentials
}

if cfg.TorEnabled {
torDialer := &tor.DefaultDialer{}
socks5Config.Dial = torDialer.Dial
logger.Info().Msg("Tor mode enabled")

torController := tor.NewTorController(torDialer)
ch := make(chan bool)
go tor.SwitcherIdentity(&logger, torController, cfg.TorIdentityInterval, ch)

go func() {
<-ch
logger.Fatal().Msg("Tor identity switcher stopped")
}()
}

sock5Server := socks5.New(&socks5Config)

logger.Info().Msgf("Starting socks5 server on %s://%s", cfg.Network, cfg.ADDR)
Expand Down
14 changes: 8 additions & 6 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package config
import "time"

type Config struct {
Timezone string `env:"TZ" envDefault:"Local"`
Network string `env:"NETWORK" envDefault:"tcp"`
ADDR string `env:"ADDR" envDefault:":1080"`
Credentials []string `env:"CREDENTIALS" envSeparator:","`
ClientTimeout time.Duration `env:"CLIENT_TIMEOUT" envDefault:"15s"`
DestTimeout time.Duration `env:"DEST_TIMEOUT" envDefault:"15s"`
Timezone string `env:"TZ" envDefault:"Local"`
Network string `env:"NETWORK" envDefault:"tcp"`
ADDR string `env:"ADDR" envDefault:":1080"`
Credentials []string `env:"CREDENTIALS" envSeparator:","`
ClientTimeout time.Duration `env:"CLIENT_TIMEOUT" envDefault:"15s"`
DestTimeout time.Duration `env:"DEST_TIMEOUT" envDefault:"15s"`
TorEnabled bool `env:"TOR_ENABLED" envDefault:"false"`
TorIdentityInterval time.Duration `env:"TOR_IDENTITY_INTERVAL" envDefault:"10m"`
}
43 changes: 43 additions & 0 deletions pkg/tor/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package tor

import (
"bufio"
"fmt"
"github.com/rs/zerolog"
)

type Controller struct {
dialer Dialer
}

func NewTorController(dialer Dialer) *Controller {
return &Controller{dialer: dialer}
}

func (t *Controller) RequestNewTorIdentity(logger *zerolog.Logger) error {
conn, err := t.dialer.Dial("tcp", "127.0.0.1:9051")
if err != nil {
return fmt.Errorf("failed to connect to tor control port: %w", err)
}
defer conn.Close()

_, _ = fmt.Fprintf(conn, "AUTHENTICATE\r\n")
_, _ = fmt.Fprintf(conn, "SIGNAL NEWNYM\r\n")

authStatus, err := bufio.NewReader(conn).ReadString('\n')
if err != nil || authStatus != "250 OK\r\n" {
return fmt.Errorf("failed to authenticate with tor control port: %w", err)
}

_, _ = fmt.Fprintf(conn, "SIGNAL NEWNYM\r\n")
status, err := bufio.NewReader(conn).ReadString('\n')
if err != nil || status != "250 OK\r\n" {
return fmt.Errorf("failed to switch tor identity: %w", err)
}

if logger != nil {
logger.Info().Msg("Tor identity changed")
}

return nil
}
68 changes: 68 additions & 0 deletions pkg/tor/controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package tor_test

import (
"bytes"
"fmt"
"github.com/ryanbekhen/nanoproxy/pkg/tor"
"net"
"testing"

"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
)

type MockConn struct {
net.Conn
responses []string
writeBuf []string
index int
}

func (mc *MockConn) Read(b []byte) (n int, err error) {
if mc.index >= len(mc.responses) {
return 0, fmt.Errorf("EOF")
}
copy(b, mc.responses[mc.index])
mc.index++
return len(mc.responses[mc.index-1]), nil
}

func (mc *MockConn) Write(b []byte) (n int, err error) {
mc.writeBuf = append(mc.writeBuf, string(b))
return len(b), nil
}

func (mc *MockConn) Close() error {
return nil
}

type MockDialer struct {
net.Conn
shouldFail bool
}

func (md *MockDialer) Dial(network, address string) (net.Conn, error) {
if md.shouldFail {
return nil, fmt.Errorf("failed to connect to tor control port")
}
return &MockConn{responses: []string{"250 OK\r\n", "250 OK\r\n"}}, nil
}

func TestRequestNewTorIdentity_Success(t *testing.T) {
logger := zerolog.New(zerolog.ConsoleWriter{Out: &bytes.Buffer{}}).With().Logger()
dialer := &MockDialer{shouldFail: false}
torController := tor.NewTorController(dialer)

err := torController.RequestNewTorIdentity(&logger)
assert.Nil(t, err, "expected no error during successful RequestNewTorIdentity call")
}

func TestRequestNewTorIdentity_FailConnect(t *testing.T) {
logger := zerolog.New(zerolog.ConsoleWriter{Out: &bytes.Buffer{}}).With().Logger()
dialer := &MockDialer{shouldFail: true}
torController := tor.NewTorController(dialer)

err := torController.RequestNewTorIdentity(&logger)
assert.NotNil(t, err, "expected error when connection fails")
assert.Contains(t, err.Error(), "failed to connect to tor control port")
}
24 changes: 24 additions & 0 deletions pkg/tor/dial.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package tor

import (
"fmt"
"golang.org/x/net/proxy"
"net"
)

var customSOCKS5 = proxy.SOCKS5

type Dialer interface {
Dial(network, address string) (net.Conn, error)
}

type DefaultDialer struct{}

func (d DefaultDialer) Dial(network, address string) (net.Conn, error) {
dialer, err := customSOCKS5("tcp", "localhost:9050", nil, proxy.Direct)
if err != nil {
return nil, fmt.Errorf("failed to create tor dialer: %w", err)
}

return dialer.Dial(network, address)
}
Loading
Loading