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

Add support for http proxy #68

Merged
merged 10 commits into from
May 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,12 @@ jobs:
run: ./wireproxy -c test.conf & sleep 1
- name: Test socks5
run: curl --proxy socks5://localhost:64423 http://zx2c4.com/ip | grep -q "demo.wireguard.com"

- name: Test http
run: curl --proxy http://localhost:64424 http://zx2c4.com/ip | grep -q "demo.wireguard.com"
- name: Test http with password
run: curl --proxy http://peter:hunter123@localhost:64424 http://zx2c4.com/ip | grep -q "demo.wireguard.com"
- name: Test http with wrong password
run: |
set +e
curl -s --fail --proxy http://peter:wrongpass@localhost:64425 http://zx2c4.com/ip
if [[ $? == 0 ]]; then exit 1; fi
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
[![Build status](https://github.com/octeep/wireproxy/actions/workflows/build.yml/badge.svg)](https://github.com/octeep/wireproxy/actions)
[![Documentation](https://img.shields.io/badge/godoc-wireproxy-blue)](https://pkg.go.dev/github.com/octeep/wireproxy)

A wireguard client that exposes itself as a socks5 proxy or tunnels.
A wireguard client that exposes itself as a socks5/http proxy or tunnels.

# What is this
`wireproxy` is a completely userspace application that connects to a wireguard peer,
and exposes a socks5 proxy or tunnels on the machine. This can be useful if you need
and exposes a socks5/http proxy or tunnels on the machine. This can be useful if you need
to connect to certain sites via a wireguard peer, but can't be bothered to setup a new network
interface for whatever reasons.

Expand All @@ -22,7 +22,7 @@ anything.

# Feature
- TCP static routing for client and server
- SOCKS5 proxy (currently only CONNECT is supported)
- SOCKS5/HTTP proxy (currently only CONNECT is supported)

# TODO
- UDP Support in SOCKS5
Expand Down Expand Up @@ -100,6 +100,16 @@ BindAddress = 127.0.0.1:25344
#Username = ...
# Avoid using spaces in the password field
#Password = ...

# http creates a http proxy on your LAN, and all traffic would be routed via wireguard.
[http]
BindAddress = 127.0.0.1:25345

# HTTP authentication parameters, specifying username and password enables
# proxy authentication.
#Username = ...
# Avoid using spaces in the password field
#Password = ...
```

Alternatively, if you already have a wireguard config, you can import it in the
Expand Down
29 changes: 29 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ type Socks5Config struct {
Password string
}

type HTTPConfig struct {
BindAddress string
Username string
Password string
}

type Configuration struct {
Device *DeviceConfig
Routines []RoutineSpawner
Expand Down Expand Up @@ -330,6 +336,24 @@ func parseSocks5Config(section *ini.Section) (RoutineSpawner, error) {
return config, nil
}

func parseHTTPConfig(section *ini.Section) (RoutineSpawner, error) {
config := &HTTPConfig{}

bindAddress, err := parseString(section, "BindAddress")
if err != nil {
return nil, err
}
config.BindAddress = bindAddress

username, _ := parseString(section, "Username")
config.Username = username

password, _ := parseString(section, "Password")
config.Password = password

return config, nil
}

// Takes a function that parses an individual section into a config, and apply it on all
// specified sections
func parseRoutinesConfig(routines *[]RoutineSpawner, cfg *ini.File, sectionName string, f func(*ini.Section) (RoutineSpawner, error)) error {
Expand Down Expand Up @@ -404,6 +428,11 @@ func ParseConfig(path string) (*Configuration, error) {
return nil, err
}

err = parseRoutinesConfig(&routinesSpawners, cfg, "http", parseHTTPConfig)
if err != nil {
return nil, err
}

return &Configuration{
Device: device,
Routines: routinesSpawners,
Expand Down
156 changes: 156 additions & 0 deletions http.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package wireproxy

import (
"bufio"
"bytes"
"encoding/base64"
"fmt"
"io"
"log"
"net"
"net/http"
"strings"
)

const proxyAuthHeaderKey = "Proxy-Authorization"

type HTTPServer struct {
config *HTTPConfig

auth CredentialValidator
dial func(network, address string) (net.Conn, error)

authRequired bool
}

func (s *HTTPServer) authenticate(req *http.Request) (int, error) {
if !s.authRequired {
return 0, nil
}

auth := req.Header.Get(proxyAuthHeaderKey)
if auth != "" {
enc := strings.TrimPrefix(auth, "Basic ")
str, err := base64.StdEncoding.DecodeString(enc)
if err != nil {
return http.StatusNotAcceptable, fmt.Errorf("decode username and password failed: %w", err)
}
pairs := bytes.SplitN(str, []byte(":"), 2)
if len(pairs) != 2 {
return http.StatusLengthRequired, fmt.Errorf("username and password format invalid")
}
if s.auth.Valid(string(pairs[0]), string(pairs[1])) {
return 0, nil
}
return http.StatusUnauthorized, fmt.Errorf("username and password not matching")
}

return http.StatusProxyAuthRequired, fmt.Errorf(http.StatusText(http.StatusProxyAuthRequired))
}

func (s *HTTPServer) handleConn(req *http.Request, conn net.Conn) (peer net.Conn, err error) {
addr := req.Host
if !strings.Contains(addr, ":") {
port := "443"
addr = net.JoinHostPort(addr, port)
}

peer, err = s.dial("tcp", addr)
if err != nil {
return peer, fmt.Errorf("tun tcp dial failed: %w", err)
}

_, err = conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
if err != nil {
peer.Close()
peer = nil
}

return
}

func (s *HTTPServer) handle(req *http.Request) (peer net.Conn, err error) {
addr := req.Host
if !strings.Contains(addr, ":") {
port := "80"
addr = net.JoinHostPort(addr, port)
}

peer, err = s.dial("tcp", addr)
if err != nil {
return peer, fmt.Errorf("tun tcp dial failed: %w", err)
}

err = req.Write(peer)
if err != nil {
peer.Close()
peer = nil
return peer, fmt.Errorf("conn write failed: %w", err)
}

return
}

func (s *HTTPServer) serve(conn net.Conn) error {
defer conn.Close()

var rd io.Reader = bufio.NewReader(conn)
req, err := http.ReadRequest(rd.(*bufio.Reader))
if err != nil {
return fmt.Errorf("read request failed: %w", err)
}

code, err := s.authenticate(req)
if err != nil {
_ = responseWith(req, code).Write(conn)
return err
}

var peer net.Conn
switch req.Method {
case http.MethodConnect:
peer, err = s.handleConn(req, conn)
case http.MethodGet:
peer, err = s.handle(req)
default:
_ = responseWith(req, http.StatusMethodNotAllowed).Write(conn)
return fmt.Errorf("unsupported protocol: %s", req.Method)
}
if err != nil {
return fmt.Errorf("dial proxy failed: %w", err)
}
if peer == nil {
return fmt.Errorf("dial proxy failed: peer nil")
}
defer peer.Close()

go func() {
defer peer.Close()
defer conn.Close()
_, _ = io.Copy(conn, peer)
}()
_, err = io.Copy(peer, conn)

return err
}

// ListenAndServe is used to create a listener and serve on it
func (s *HTTPServer) ListenAndServe(network, addr string) error {
server, err := net.Listen("tcp", s.config.BindAddress)
if err != nil {
return fmt.Errorf("listen tcp failed: %w", err)
}

for {
conn, err := server.Accept()
if err != nil {
return fmt.Errorf("accept request failed: %w", err)
}
go func(conn net.Conn) {
err = s.serve(conn)
if err != nil {
log.Println(err)
}
}(conn)
}
}
16 changes: 16 additions & 0 deletions routine.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,22 @@ func (config *Socks5Config) SpawnRoutine(vt *VirtualTun) {
}
}

// SpawnRoutine spawns a http server.
func (config *HTTPConfig) SpawnRoutine(vt *VirtualTun) {
http := &HTTPServer{
config: config,
dial: vt.Tnet.Dial,
auth: CredentialValidator{config.Username, config.Password},
}
if config.Username != "" || config.Password != "" {
http.authRequired = true
}

if err := http.ListenAndServe("tcp", config.BindAddress); err != nil {
log.Fatal(err)
}
}

// Valid checks the authentication data in CredentialValidator and compare them
// to username and password in constant time.
func (c CredentialValidator) Valid(username, password string) bool {
Expand Down
8 changes: 8 additions & 0 deletions test_config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,12 @@ Endpoint = demo.wireguard.com:$server_port

[Socks5]
BindAddress = 127.0.0.1:64423

[http]
BindAddress = 127.0.0.1:64424

[http]
BindAddress = 127.0.0.1:64425
Username = peter
Password = hunter123
EOL
25 changes: 25 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package wireproxy

import (
"bytes"
"io"
"net/http"
"strconv"
)

const space = " "

func responseWith(req *http.Request, statusCode int) *http.Response {
statusText := http.StatusText(statusCode)
body := "wireproxy:" + space + req.Proto + space + strconv.Itoa(statusCode) + space + statusText + "\r\n"

return &http.Response{
StatusCode: statusCode,
Status: statusText,
Proto: req.Proto,
ProtoMajor: req.ProtoMajor,
ProtoMinor: req.ProtoMinor,
Header: http.Header{},
Body: io.NopCloser(bytes.NewBufferString(body)),
}
}