Skip to content

Commit 25e6568

Browse files
Add support for http proxy (#68)
* Add support for http proxy * add test case for http proxy --------- Co-authored-by: octeep <[email protected]> Co-authored-by: pufferfish <[email protected]>
1 parent d9c6eb7 commit 25e6568

File tree

7 files changed

+256
-4
lines changed

7 files changed

+256
-4
lines changed

.github/workflows/test.yml

+9-1
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,12 @@ jobs:
2929
run: ./wireproxy -c test.conf & sleep 1
3030
- name: Test socks5
3131
run: curl --proxy socks5://localhost:64423 http://zx2c4.com/ip | grep -q "demo.wireguard.com"
32-
32+
- name: Test http
33+
run: curl --proxy http://localhost:64424 http://zx2c4.com/ip | grep -q "demo.wireguard.com"
34+
- name: Test http with password
35+
run: curl --proxy http://peter:hunter123@localhost:64424 http://zx2c4.com/ip | grep -q "demo.wireguard.com"
36+
- name: Test http with wrong password
37+
run: |
38+
set +e
39+
curl -s --fail --proxy http://peter:wrongpass@localhost:64425 http://zx2c4.com/ip
40+
if [[ $? == 0 ]]; then exit 1; fi

README.md

+13-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
[![Build status](https://github.com/octeep/wireproxy/actions/workflows/build.yml/badge.svg)](https://github.com/octeep/wireproxy/actions)
44
[![Documentation](https://img.shields.io/badge/godoc-wireproxy-blue)](https://pkg.go.dev/github.com/octeep/wireproxy)
55

6-
A wireguard client that exposes itself as a socks5 proxy or tunnels.
6+
A wireguard client that exposes itself as a socks5/http proxy or tunnels.
77

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

@@ -22,7 +22,7 @@ anything.
2222

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

2727
# TODO
2828
- UDP Support in SOCKS5
@@ -100,6 +100,16 @@ BindAddress = 127.0.0.1:25344
100100
#Username = ...
101101
# Avoid using spaces in the password field
102102
#Password = ...
103+
104+
# http creates a http proxy on your LAN, and all traffic would be routed via wireguard.
105+
[http]
106+
BindAddress = 127.0.0.1:25345
107+
108+
# HTTP authentication parameters, specifying username and password enables
109+
# proxy authentication.
110+
#Username = ...
111+
# Avoid using spaces in the password field
112+
#Password = ...
103113
```
104114

105115
Alternatively, if you already have a wireguard config, you can import it in the

config.go

+29
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ type Socks5Config struct {
4545
Password string
4646
}
4747

48+
type HTTPConfig struct {
49+
BindAddress string
50+
Username string
51+
Password string
52+
}
53+
4854
type Configuration struct {
4955
Device *DeviceConfig
5056
Routines []RoutineSpawner
@@ -330,6 +336,24 @@ func parseSocks5Config(section *ini.Section) (RoutineSpawner, error) {
330336
return config, nil
331337
}
332338

339+
func parseHTTPConfig(section *ini.Section) (RoutineSpawner, error) {
340+
config := &HTTPConfig{}
341+
342+
bindAddress, err := parseString(section, "BindAddress")
343+
if err != nil {
344+
return nil, err
345+
}
346+
config.BindAddress = bindAddress
347+
348+
username, _ := parseString(section, "Username")
349+
config.Username = username
350+
351+
password, _ := parseString(section, "Password")
352+
config.Password = password
353+
354+
return config, nil
355+
}
356+
333357
// Takes a function that parses an individual section into a config, and apply it on all
334358
// specified sections
335359
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) {
404428
return nil, err
405429
}
406430

431+
err = parseRoutinesConfig(&routinesSpawners, cfg, "http", parseHTTPConfig)
432+
if err != nil {
433+
return nil, err
434+
}
435+
407436
return &Configuration{
408437
Device: device,
409438
Routines: routinesSpawners,

http.go

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package wireproxy
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"encoding/base64"
7+
"fmt"
8+
"io"
9+
"log"
10+
"net"
11+
"net/http"
12+
"strings"
13+
)
14+
15+
const proxyAuthHeaderKey = "Proxy-Authorization"
16+
17+
type HTTPServer struct {
18+
config *HTTPConfig
19+
20+
auth CredentialValidator
21+
dial func(network, address string) (net.Conn, error)
22+
23+
authRequired bool
24+
}
25+
26+
func (s *HTTPServer) authenticate(req *http.Request) (int, error) {
27+
if !s.authRequired {
28+
return 0, nil
29+
}
30+
31+
auth := req.Header.Get(proxyAuthHeaderKey)
32+
if auth != "" {
33+
enc := strings.TrimPrefix(auth, "Basic ")
34+
str, err := base64.StdEncoding.DecodeString(enc)
35+
if err != nil {
36+
return http.StatusNotAcceptable, fmt.Errorf("decode username and password failed: %w", err)
37+
}
38+
pairs := bytes.SplitN(str, []byte(":"), 2)
39+
if len(pairs) != 2 {
40+
return http.StatusLengthRequired, fmt.Errorf("username and password format invalid")
41+
}
42+
if s.auth.Valid(string(pairs[0]), string(pairs[1])) {
43+
return 0, nil
44+
}
45+
return http.StatusUnauthorized, fmt.Errorf("username and password not matching")
46+
}
47+
48+
return http.StatusProxyAuthRequired, fmt.Errorf(http.StatusText(http.StatusProxyAuthRequired))
49+
}
50+
51+
func (s *HTTPServer) handleConn(req *http.Request, conn net.Conn) (peer net.Conn, err error) {
52+
addr := req.Host
53+
if !strings.Contains(addr, ":") {
54+
port := "443"
55+
addr = net.JoinHostPort(addr, port)
56+
}
57+
58+
peer, err = s.dial("tcp", addr)
59+
if err != nil {
60+
return peer, fmt.Errorf("tun tcp dial failed: %w", err)
61+
}
62+
63+
_, err = conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
64+
if err != nil {
65+
peer.Close()
66+
peer = nil
67+
}
68+
69+
return
70+
}
71+
72+
func (s *HTTPServer) handle(req *http.Request) (peer net.Conn, err error) {
73+
addr := req.Host
74+
if !strings.Contains(addr, ":") {
75+
port := "80"
76+
addr = net.JoinHostPort(addr, port)
77+
}
78+
79+
peer, err = s.dial("tcp", addr)
80+
if err != nil {
81+
return peer, fmt.Errorf("tun tcp dial failed: %w", err)
82+
}
83+
84+
err = req.Write(peer)
85+
if err != nil {
86+
peer.Close()
87+
peer = nil
88+
return peer, fmt.Errorf("conn write failed: %w", err)
89+
}
90+
91+
return
92+
}
93+
94+
func (s *HTTPServer) serve(conn net.Conn) error {
95+
defer conn.Close()
96+
97+
var rd io.Reader = bufio.NewReader(conn)
98+
req, err := http.ReadRequest(rd.(*bufio.Reader))
99+
if err != nil {
100+
return fmt.Errorf("read request failed: %w", err)
101+
}
102+
103+
code, err := s.authenticate(req)
104+
if err != nil {
105+
_ = responseWith(req, code).Write(conn)
106+
return err
107+
}
108+
109+
var peer net.Conn
110+
switch req.Method {
111+
case http.MethodConnect:
112+
peer, err = s.handleConn(req, conn)
113+
case http.MethodGet:
114+
peer, err = s.handle(req)
115+
default:
116+
_ = responseWith(req, http.StatusMethodNotAllowed).Write(conn)
117+
return fmt.Errorf("unsupported protocol: %s", req.Method)
118+
}
119+
if err != nil {
120+
return fmt.Errorf("dial proxy failed: %w", err)
121+
}
122+
if peer == nil {
123+
return fmt.Errorf("dial proxy failed: peer nil")
124+
}
125+
defer peer.Close()
126+
127+
go func() {
128+
defer peer.Close()
129+
defer conn.Close()
130+
_, _ = io.Copy(conn, peer)
131+
}()
132+
_, err = io.Copy(peer, conn)
133+
134+
return err
135+
}
136+
137+
// ListenAndServe is used to create a listener and serve on it
138+
func (s *HTTPServer) ListenAndServe(network, addr string) error {
139+
server, err := net.Listen("tcp", s.config.BindAddress)
140+
if err != nil {
141+
return fmt.Errorf("listen tcp failed: %w", err)
142+
}
143+
144+
for {
145+
conn, err := server.Accept()
146+
if err != nil {
147+
return fmt.Errorf("accept request failed: %w", err)
148+
}
149+
go func(conn net.Conn) {
150+
err = s.serve(conn)
151+
if err != nil {
152+
log.Println(err)
153+
}
154+
}(conn)
155+
}
156+
}

routine.go

+16
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,22 @@ func (config *Socks5Config) SpawnRoutine(vt *VirtualTun) {
137137
}
138138
}
139139

140+
// SpawnRoutine spawns a http server.
141+
func (config *HTTPConfig) SpawnRoutine(vt *VirtualTun) {
142+
http := &HTTPServer{
143+
config: config,
144+
dial: vt.Tnet.Dial,
145+
auth: CredentialValidator{config.Username, config.Password},
146+
}
147+
if config.Username != "" || config.Password != "" {
148+
http.authRequired = true
149+
}
150+
151+
if err := http.ListenAndServe("tcp", config.BindAddress); err != nil {
152+
log.Fatal(err)
153+
}
154+
}
155+
140156
// Valid checks the authentication data in CredentialValidator and compare them
141157
// to username and password in constant time.
142158
func (c CredentialValidator) Valid(username, password string) bool {

test_config.sh

+8
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,12 @@ Endpoint = demo.wireguard.com:$server_port
1717
1818
[Socks5]
1919
BindAddress = 127.0.0.1:64423
20+
21+
[http]
22+
BindAddress = 127.0.0.1:64424
23+
24+
[http]
25+
BindAddress = 127.0.0.1:64425
26+
Username = peter
27+
Password = hunter123
2028
EOL

util.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package wireproxy
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"net/http"
7+
"strconv"
8+
)
9+
10+
const space = " "
11+
12+
func responseWith(req *http.Request, statusCode int) *http.Response {
13+
statusText := http.StatusText(statusCode)
14+
body := "wireproxy:" + space + req.Proto + space + strconv.Itoa(statusCode) + space + statusText + "\r\n"
15+
16+
return &http.Response{
17+
StatusCode: statusCode,
18+
Status: statusText,
19+
Proto: req.Proto,
20+
ProtoMajor: req.ProtoMajor,
21+
ProtoMinor: req.ProtoMinor,
22+
Header: http.Header{},
23+
Body: io.NopCloser(bytes.NewBufferString(body)),
24+
}
25+
}

0 commit comments

Comments
 (0)