Fix linter issues
This commit is contained in:
@@ -4,6 +4,7 @@ run:
|
|||||||
linters:
|
linters:
|
||||||
default: all
|
default: all
|
||||||
disable:
|
disable:
|
||||||
|
- errcheck
|
||||||
- gomodguard
|
- gomodguard
|
||||||
- wsl
|
- wsl
|
||||||
- revive
|
- revive
|
||||||
|
|||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2026 Vladimir "hdkv" Hodakov
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# deconnect
|
||||||
|
|
||||||
|
A tool to make sure your super-duper-enterprise proxy accepts your requests.
|
||||||
@@ -29,9 +29,9 @@ func (a *App) Logger() *logrus.Entry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func New(ctx context.Context) *App {
|
func New(ctx context.Context) *App {
|
||||||
var m runtime.MemStats
|
var memStats runtime.MemStats
|
||||||
|
|
||||||
runtime.ReadMemStats(&m)
|
runtime.ReadMemStats(&memStats)
|
||||||
|
|
||||||
app := new(App)
|
app := new(App)
|
||||||
|
|
||||||
@@ -43,9 +43,9 @@ func New(ctx context.Context) *App {
|
|||||||
})
|
})
|
||||||
|
|
||||||
app.logger = logger.WithContext(ctx).WithFields(logrus.Fields{
|
app.logger = logger.WithContext(ctx).WithFields(logrus.Fields{
|
||||||
"memalloc": fmt.Sprintf("%dMB", m.Alloc/1024/1024),
|
"memalloc": fmt.Sprintf("%dMB", memStats.Alloc/1024/1024),
|
||||||
"memsys": fmt.Sprintf("%dMB", m.Sys/1024/1024),
|
"memsys": fmt.Sprintf("%dMB", memStats.Sys/1024/1024),
|
||||||
"numgc": strconv.FormatUint(uint64(m.NumGC), 10),
|
"numgc": strconv.FormatUint(uint64(memStats.NumGC), 10),
|
||||||
})
|
})
|
||||||
|
|
||||||
app.domains = make(map[string]domains.Domain)
|
app.domains = make(map[string]domains.Domain)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package domains
|
package domains
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net"
|
"net"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DomainNameDeconnector = "deconnector"
|
const DomainNameDeconnector = "deconnector"
|
||||||
|
|
||||||
type Deconnector interface {
|
type Deconnector interface {
|
||||||
Handle(clientConn net.Conn)
|
Handle(ctx context.Context, clientConn net.Conn)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ package deconnector
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Deconnector) handleDeconnect(clientConn net.Conn, connectReq *http.Request, upstreamURL *url.URL) {
|
func (d *Deconnector) handleDeconnect(ctx context.Context, clientConn net.Conn, connectReq *http.Request) {
|
||||||
// Tell client the tunnel is open
|
// Tell client the tunnel is open
|
||||||
fmt.Fprintf(clientConn, "HTTP/1.1 200 Connection established\r\n\r\n")
|
_, _ = fmt.Fprintf(clientConn, "HTTP/1.1 200 Connection established\r\n\r\n")
|
||||||
|
|
||||||
// Read the real HTTP request the client sends through the tunnel
|
// Read the real HTTP request the client sends through the tunnel
|
||||||
innerReq, err := http.ReadRequest(bufio.NewReader(clientConn))
|
innerReq, err := http.ReadRequest(bufio.NewReader(clientConn))
|
||||||
@@ -30,5 +30,5 @@ func (d *Deconnector) handleDeconnect(clientConn net.Conn, connectReq *http.Requ
|
|||||||
WithField("url", innerReq.URL).
|
WithField("url", innerReq.URL).
|
||||||
Info("Handling de-CONNECT request")
|
Info("Handling de-CONNECT request")
|
||||||
|
|
||||||
d.forwardHTTP(clientConn, innerReq, upstreamURL)
|
d.forwardHTTP(ctx, clientConn, innerReq)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ package deconnector
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Deconnector) forwardHTTP(clientConn net.Conn, req *http.Request, upstreamURL *url.URL) {
|
func (d *Deconnector) forwardHTTP(ctx context.Context, clientConn net.Conn, req *http.Request) {
|
||||||
upstreamConn, err := d.dialer.Dial()
|
upstreamConn, err := d.dialer.Dial(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.Logger().WithError(err).Error("upstream dial failed")
|
d.app.Logger().WithError(err).Error("upstream dial failed")
|
||||||
fmt.Fprintf(clientConn, "HTTP/1.1 502 Bad Gateway\r\n\r\n")
|
fmt.Fprintf(clientConn, "HTTP/1.1 502 Bad Gateway\r\n\r\n")
|
||||||
@@ -18,6 +18,10 @@ func (d *Deconnector) forwardHTTP(clientConn net.Conn, req *http.Request, upstre
|
|||||||
}
|
}
|
||||||
defer upstreamConn.Close()
|
defer upstreamConn.Close()
|
||||||
|
|
||||||
|
if authHeader, ok := d.dialer.Auth(); ok {
|
||||||
|
req.Header.Set("Proxy-Authorization", "Basic "+authHeader)
|
||||||
|
}
|
||||||
|
|
||||||
if err := req.WriteProxy(upstreamConn); err != nil {
|
if err := req.WriteProxy(upstreamConn); err != nil {
|
||||||
d.app.Logger().WithError(err).Error("failed to write request")
|
d.app.Logger().WithError(err).Error("failed to write request")
|
||||||
|
|
||||||
@@ -33,5 +37,8 @@ func (d *Deconnector) forwardHTTP(clientConn net.Conn, req *http.Request, upstre
|
|||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
resp.Write(clientConn)
|
err = resp.Write(clientConn)
|
||||||
|
if err != nil {
|
||||||
|
d.app.Logger().WithError(err).Error("failed to write response")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,12 @@ package deconnector
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Deconnector) Handle(clientConn net.Conn) {
|
func (d *Deconnector) Handle(ctx context.Context, clientConn net.Conn) {
|
||||||
upstreamURL, err := d.dialer.UpstreamURL()
|
|
||||||
if err != nil {
|
|
||||||
d.app.Logger().WithError(err).Error("failed to get upstream URL")
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer clientConn.Close()
|
defer clientConn.Close()
|
||||||
|
|
||||||
req, err := http.ReadRequest(bufio.NewReader(clientConn))
|
req, err := http.ReadRequest(bufio.NewReader(clientConn))
|
||||||
@@ -26,9 +20,9 @@ func (d *Deconnector) Handle(clientConn net.Conn) {
|
|||||||
if req.Method == http.MethodConnect {
|
if req.Method == http.MethodConnect {
|
||||||
_, port, _ := net.SplitHostPort(req.Host)
|
_, port, _ := net.SplitHostPort(req.Host)
|
||||||
if port == "443" {
|
if port == "443" {
|
||||||
d.handleTunnel(clientConn, req.Host, upstreamURL)
|
d.handleTunnel(ctx, clientConn, req.Host)
|
||||||
} else {
|
} else {
|
||||||
d.handleDeconnect(clientConn, req, upstreamURL)
|
d.handleDeconnect(ctx, clientConn, req)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
req.RequestURI = ""
|
req.RequestURI = ""
|
||||||
@@ -42,6 +36,6 @@ func (d *Deconnector) Handle(clientConn net.Conn) {
|
|||||||
WithField("url", req.URL).
|
WithField("url", req.URL).
|
||||||
Info("Forwarding HTTP request")
|
Info("Forwarding HTTP request")
|
||||||
|
|
||||||
d.forwardHTTP(clientConn, req, upstreamURL)
|
d.forwardHTTP(ctx, clientConn, req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,18 @@ package deconnector
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Deconnector) handleTunnel(clientConn net.Conn, host string, upstreamURL *url.URL) {
|
//nolint:funlen
|
||||||
|
func (d *Deconnector) handleTunnel(ctx context.Context, clientConn net.Conn, host string) {
|
||||||
d.app.Logger().WithField("host", host).Info("Handling CONNECT tunnel")
|
d.app.Logger().WithField("host", host).Info("Handling CONNECT tunnel")
|
||||||
|
|
||||||
upstreamConn, err := d.dialer.Dial()
|
upstreamConn, err := d.dialer.Dial(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.app.Logger().WithError(err).Error("upstream dial failed")
|
d.app.Logger().WithError(err).Error("upstream dial failed")
|
||||||
fmt.Fprintf(clientConn, "HTTP/1.1 502 Bad Gateway\r\n\r\n")
|
fmt.Fprintf(clientConn, "HTTP/1.1 502 Bad Gateway\r\n\r\n")
|
||||||
@@ -24,6 +25,10 @@ func (d *Deconnector) handleTunnel(clientConn net.Conn, host string, upstreamURL
|
|||||||
connectLine := fmt.Sprintf(
|
connectLine := fmt.Sprintf(
|
||||||
"CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", host, host,
|
"CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", host, host,
|
||||||
)
|
)
|
||||||
|
if authHeader, ok := d.dialer.Auth(); ok {
|
||||||
|
connectLine += fmt.Sprintf("Proxy-Authorization: Basic %s\r\n", authHeader)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Fprint(upstreamConn, connectLine)
|
fmt.Fprint(upstreamConn, connectLine)
|
||||||
|
|
||||||
resp, err := http.ReadResponse(bufio.NewReader(upstreamConn), nil)
|
resp, err := http.ReadResponse(bufio.NewReader(upstreamConn), nil)
|
||||||
@@ -33,13 +38,40 @@ func (d *Deconnector) handleTunnel(clientConn net.Conn, host string, upstreamURL
|
|||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
fmt.Fprintf(clientConn, "HTTP/1.1 200 Connection established\r\n\r\n")
|
fmt.Fprintf(clientConn, "HTTP/1.1 200 Connection established\r\n\r\n")
|
||||||
|
|
||||||
done := make(chan struct{}, 2)
|
done := make(chan struct{}, 2)
|
||||||
|
|
||||||
go func() { io.Copy(upstreamConn, clientConn); done <- struct{}{} }()
|
go func() {
|
||||||
go func() { io.Copy(clientConn, upstreamConn); done <- struct{}{} }()
|
defer func() {
|
||||||
|
_ = upstreamConn.Close()
|
||||||
|
_ = clientConn.Close()
|
||||||
|
|
||||||
|
done <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, err := io.Copy(upstreamConn, clientConn); err != nil {
|
||||||
|
d.app.Logger().
|
||||||
|
WithError(err).
|
||||||
|
Debug("client -> upstream copy stopped")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
_ = upstreamConn.Close()
|
||||||
|
_ = clientConn.Close()
|
||||||
|
|
||||||
|
done <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if _, err := io.Copy(clientConn, upstreamConn); err != nil {
|
||||||
|
d.app.Logger().
|
||||||
|
WithError(err).
|
||||||
|
Debug("upstream -> client copy stopped")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
<-done
|
<-done
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package domains
|
package domains
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
)
|
)
|
||||||
@@ -8,6 +9,7 @@ import (
|
|||||||
const DomainNameDialer = "dialer"
|
const DomainNameDialer = "dialer"
|
||||||
|
|
||||||
type Dialer interface {
|
type Dialer interface {
|
||||||
Dial() (net.Conn, error)
|
Auth() (string, bool)
|
||||||
|
Dial(ctx context.Context) (net.Conn, error)
|
||||||
UpstreamURL() (*url.URL, error)
|
UpstreamURL() (*url.URL, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,52 @@
|
|||||||
package dialer
|
package dialer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (d *Dialer) Dial() (net.Conn, error) {
|
func (d *Dialer) Auth() (string, bool) {
|
||||||
if d.dialURL.Scheme == "https" {
|
url, _ := d.UpstreamURL()
|
||||||
return tls.Dial("tcp", d.dialURL.Host, &tls.Config{
|
if url.User == nil {
|
||||||
ServerName: d.dialURL.Hostname(),
|
return "", false
|
||||||
InsecureSkipVerify: d.app.Config().Upstream.InsecureTLS,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return net.Dial("tcp", d.dialURL.Host)
|
username := url.User.Username()
|
||||||
|
password, _ := url.User.Password()
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(
|
||||||
|
[]byte(username + ":" + password),
|
||||||
|
), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dialer) Dial(ctx context.Context) (net.Conn, error) {
|
||||||
|
dialer := &net.Dialer{}
|
||||||
|
|
||||||
|
conn, err := dialer.DialContext(ctx, "tcp", d.dialURL.Host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %w (%w)", ErrDialer, ErrDial, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.dialURL.Scheme != "https" {
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConn := tls.Client(conn, &tls.Config{
|
||||||
|
ServerName: d.dialURL.Hostname(),
|
||||||
|
//nolint:gosec
|
||||||
|
InsecureSkipVerify: d.app.Config().Upstream.InsecureTLS,
|
||||||
|
})
|
||||||
|
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("%w: %w (%w)", ErrDialer, ErrDial, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlsConn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Dialer) UpstreamURL() (*url.URL, error) {
|
func (d *Dialer) UpstreamURL() (*url.URL, error) {
|
||||||
@@ -26,12 +57,12 @@ func (d *Dialer) UpstreamURL() (*url.URL, error) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.Parse(d.app.Config().Upstream.URL)
|
upstreamURL, err := url.Parse(d.app.Config().Upstream.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"%w: %w (%w)", ErrDialer, ErrUpstreamURL, err,
|
"%w: %w (%w)", ErrDialer, ErrUpstreamURL, err,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return u, nil
|
return upstreamURL, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ import "errors"
|
|||||||
var (
|
var (
|
||||||
ErrDialer = errors.New("dialer")
|
ErrDialer = errors.New("dialer")
|
||||||
ErrConnectDependencies = errors.New("function ConnectDependencies()")
|
ErrConnectDependencies = errors.New("function ConnectDependencies()")
|
||||||
|
ErrDial = errors.New("function Dial()")
|
||||||
ErrUpstreamURL = errors.New("function UpstreamURL()")
|
ErrUpstreamURL = errors.New("function UpstreamURL()")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,9 +7,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (l *Listener) Listen(ctx context.Context) error {
|
func (l *Listener) Listen(ctx context.Context) error {
|
||||||
ln, err := net.Listen(
|
listenerConfig := new(net.ListenConfig)
|
||||||
|
|
||||||
|
listener, err := listenerConfig.Listen(
|
||||||
|
ctx,
|
||||||
"tcp",
|
"tcp",
|
||||||
l.app.Config().Deconnect.Host+":"+l.app.Config().Deconnect.Port,
|
net.JoinHostPort(
|
||||||
|
l.app.Config().Deconnect.Host,
|
||||||
|
l.app.Config().Deconnect.Port,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %w (%w)", ErrListener, ErrListen, err)
|
return fmt.Errorf("%w: %w (%w)", ErrListener, ErrListen, err)
|
||||||
@@ -24,16 +30,16 @@ func (l *Listener) Listen(ctx context.Context) error {
|
|||||||
|
|
||||||
l.app.Logger().Info("Shutting down listener")
|
l.app.Logger().Info("Shutting down listener")
|
||||||
|
|
||||||
if err := ln.Close(); err != nil {
|
if err := listener.Close(); err != nil {
|
||||||
l.app.Logger().WithError(err).Error("listener close error")
|
l.app.Logger().WithError(err).Error("listener close error")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
conn, err := ln.Accept()
|
conn, err := listener.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return nil
|
return fmt.Errorf("%w: %w (%w)", ErrListener, ErrListen, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
l.app.Logger().WithError(err).Error("accept error")
|
l.app.Logger().WithError(err).Error("accept error")
|
||||||
@@ -41,6 +47,6 @@ func (l *Listener) Listen(ctx context.Context) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
go l.deconnector.Handle(conn)
|
go l.deconnector.Handle(ctx, conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user