mirror of
https://github.com/whyvl/wireproxy.git
synced 2025-04-29 19:01:42 +02:00
Add support for http proxy (#68)
* Add support for http proxy * add test case for http proxy --------- Co-authored-by: octeep <github@bandersnatch.anonaddy.com> Co-authored-by: pufferfish <74378430+pufferffish@users.noreply.github.com>
This commit is contained in:
parent
d9c6eb7143
commit
25e6568f4d
7 changed files with 256 additions and 4 deletions
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
|
@ -29,4 +29,12 @@ jobs:
|
||||||
run: ./wireproxy -c test.conf & sleep 1
|
run: ./wireproxy -c test.conf & sleep 1
|
||||||
- name: Test socks5
|
- name: Test socks5
|
||||||
run: curl --proxy socks5://localhost:64423 http://zx2c4.com/ip | grep -q "demo.wireguard.com"
|
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
README.md
16
README.md
|
@ -3,11 +3,11 @@
|
||||||
[](https://github.com/octeep/wireproxy/actions)
|
[](https://github.com/octeep/wireproxy/actions)
|
||||||
[](https://pkg.go.dev/github.com/octeep/wireproxy)
|
[](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
|
# What is this
|
||||||
`wireproxy` is a completely userspace application that connects to a wireguard peer,
|
`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
|
to connect to certain sites via a wireguard peer, but can't be bothered to setup a new network
|
||||||
interface for whatever reasons.
|
interface for whatever reasons.
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ anything.
|
||||||
|
|
||||||
# Feature
|
# Feature
|
||||||
- TCP static routing for client and server
|
- TCP static routing for client and server
|
||||||
- SOCKS5 proxy (currently only CONNECT is supported)
|
- SOCKS5/HTTP proxy (currently only CONNECT is supported)
|
||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
- UDP Support in SOCKS5
|
- UDP Support in SOCKS5
|
||||||
|
@ -100,6 +100,16 @@ BindAddress = 127.0.0.1:25344
|
||||||
#Username = ...
|
#Username = ...
|
||||||
# Avoid using spaces in the password field
|
# Avoid using spaces in the password field
|
||||||
#Password = ...
|
#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
|
Alternatively, if you already have a wireguard config, you can import it in the
|
||||||
|
|
29
config.go
29
config.go
|
@ -45,6 +45,12 @@ type Socks5Config struct {
|
||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HTTPConfig struct {
|
||||||
|
BindAddress string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
type Configuration struct {
|
type Configuration struct {
|
||||||
Device *DeviceConfig
|
Device *DeviceConfig
|
||||||
Routines []RoutineSpawner
|
Routines []RoutineSpawner
|
||||||
|
@ -330,6 +336,24 @@ func parseSocks5Config(section *ini.Section) (RoutineSpawner, error) {
|
||||||
return config, nil
|
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
|
// Takes a function that parses an individual section into a config, and apply it on all
|
||||||
// specified sections
|
// specified sections
|
||||||
func parseRoutinesConfig(routines *[]RoutineSpawner, cfg *ini.File, sectionName string, f func(*ini.Section) (RoutineSpawner, error)) error {
|
func parseRoutinesConfig(routines *[]RoutineSpawner, cfg *ini.File, sectionName string, f func(*ini.Section) (RoutineSpawner, error)) error {
|
||||||
|
@ -404,6 +428,11 @@ func ParseConfig(path string) (*Configuration, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = parseRoutinesConfig(&routinesSpawners, cfg, "http", parseHTTPConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &Configuration{
|
return &Configuration{
|
||||||
Device: device,
|
Device: device,
|
||||||
Routines: routinesSpawners,
|
Routines: routinesSpawners,
|
||||||
|
|
156
http.go
Normal file
156
http.go
Normal file
|
@ -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
routine.go
16
routine.go
|
@ -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
|
// Valid checks the authentication data in CredentialValidator and compare them
|
||||||
// to username and password in constant time.
|
// to username and password in constant time.
|
||||||
func (c CredentialValidator) Valid(username, password string) bool {
|
func (c CredentialValidator) Valid(username, password string) bool {
|
||||||
|
|
|
@ -17,4 +17,12 @@ Endpoint = demo.wireguard.com:$server_port
|
||||||
|
|
||||||
[Socks5]
|
[Socks5]
|
||||||
BindAddress = 127.0.0.1:64423
|
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
|
EOL
|
||||||
|
|
25
util.go
Normal file
25
util.go
Normal file
|
@ -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)),
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue