Release 1.8.0: auto-update with restart and AmneziaWG 2.0 support.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Navis
2026-07-29 06:45:57 +03:00
co-authored by Cursor
parent cd0b3f4707
commit 672979be4c
27 changed files with 1396 additions and 228 deletions
+551
View File
@@ -0,0 +1,551 @@
package awg
import (
"encoding/base64"
"encoding/hex"
"fmt"
"net"
"net/netip"
"strconv"
"strings"
)
// Conf is a parsed AmneziaWG / WireGuard client configuration (AWG 2.0 fields included).
type Conf struct {
PrivateKey string
Address []string
DNS []string
MTU int
Jc int
Jmin int
Jmax int
S1, S2, S3, S4 int
H1, H2, H3, H4 string
I1, I2, I3, I4, I5 string
PublicKey string
PresharedKey string
Endpoint string
AllowedIPs []string
Keepalive int
Name string
}
// Detect reports whether raw looks like an AWG/WireGuard config or share link.
func Detect(raw string) bool {
raw = strings.TrimSpace(raw)
if raw == "" {
return false
}
lower := strings.ToLower(raw)
if strings.HasPrefix(lower, "awg://") || strings.HasPrefix(lower, "amneziawg://") ||
strings.HasPrefix(lower, "wireguard://") {
return true
}
if strings.Contains(lower, "[interface]") && strings.Contains(lower, "privatekey") {
return true
}
return false
}
// Parse accepts a WireGuard/AWG .conf body or awg:// URI and returns Conf.
func Parse(raw string) (Conf, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return Conf{}, fmt.Errorf("пустой AWG конфиг")
}
lower := strings.ToLower(raw)
switch {
case strings.HasPrefix(lower, "awg://"), strings.HasPrefix(lower, "amneziawg://"),
strings.HasPrefix(lower, "wireguard://"):
return parseURI(raw)
default:
return parseINI(raw)
}
}
func parseINI(raw string) (Conf, error) {
var c Conf
section := ""
for _, line := range strings.Split(raw, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
continue
}
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
section = strings.ToLower(strings.TrimSpace(line[1 : len(line)-1]))
continue
}
key, val, ok := splitKV(line)
if !ok {
continue
}
k := strings.ToLower(key)
switch section {
case "interface":
switch k {
case "privatekey":
c.PrivateKey = val
case "address":
c.Address = appendCSV(c.Address, val)
case "dns":
c.DNS = appendCSV(c.DNS, val)
case "mtu":
c.MTU, _ = strconv.Atoi(val)
case "jc":
c.Jc, _ = strconv.Atoi(val)
case "jmin":
c.Jmin, _ = strconv.Atoi(val)
case "jmax":
c.Jmax, _ = strconv.Atoi(val)
case "s1":
c.S1, _ = strconv.Atoi(val)
case "s2":
c.S2, _ = strconv.Atoi(val)
case "s3":
c.S3, _ = strconv.Atoi(val)
case "s4":
c.S4, _ = strconv.Atoi(val)
case "h1":
c.H1 = val
case "h2":
c.H2 = val
case "h3":
c.H3 = val
case "h4":
c.H4 = val
case "i1":
c.I1 = val
case "i2":
c.I2 = val
case "i3":
c.I3 = val
case "i4":
c.I4 = val
case "i5":
c.I5 = val
}
case "peer":
switch k {
case "publickey":
c.PublicKey = val
case "presharedkey":
c.PresharedKey = val
case "endpoint":
c.Endpoint = val
case "allowedips":
c.AllowedIPs = appendCSV(c.AllowedIPs, val)
case "persistentkeepalive":
c.Keepalive, _ = strconv.Atoi(val)
}
}
}
if err := c.validate(); err != nil {
return Conf{}, err
}
return c, nil
}
func parseURI(raw string) (Conf, error) {
// awg://host:port/?public_key=...&private_key=...&address=10.8.0.2/32&jc=4&...
u := raw
schemeIdx := strings.Index(u, "://")
if schemeIdx < 0 {
return Conf{}, fmt.Errorf("bad awg uri")
}
rest := u[schemeIdx+3:]
hostPort := rest
query := ""
if i := strings.Index(rest, "?"); i >= 0 {
hostPort = rest[:i]
query = rest[i+1:]
}
hostPort = strings.TrimSuffix(hostPort, "/")
c := Conf{Endpoint: hostPort}
for _, part := range strings.Split(query, "&") {
if part == "" {
continue
}
k, v, ok := strings.Cut(part, "=")
if !ok {
continue
}
k = strings.ToLower(strings.TrimSpace(k))
v = strings.TrimSpace(v)
v, _ = urlDecode(v)
switch k {
case "private_key", "privatekey":
c.PrivateKey = v
case "public_key", "publickey":
c.PublicKey = v
case "preshared_key", "presharedkey", "psk":
c.PresharedKey = v
case "address", "addr":
c.Address = appendCSV(c.Address, v)
case "dns":
c.DNS = appendCSV(c.DNS, v)
case "allowed_ips", "allowedips":
c.AllowedIPs = appendCSV(c.AllowedIPs, v)
case "mtu":
c.MTU, _ = strconv.Atoi(v)
case "keepalive", "persistentkeepalive":
c.Keepalive, _ = strconv.Atoi(v)
case "jc":
c.Jc, _ = strconv.Atoi(v)
case "jmin":
c.Jmin, _ = strconv.Atoi(v)
case "jmax":
c.Jmax, _ = strconv.Atoi(v)
case "s1":
c.S1, _ = strconv.Atoi(v)
case "s2":
c.S2, _ = strconv.Atoi(v)
case "s3":
c.S3, _ = strconv.Atoi(v)
case "s4":
c.S4, _ = strconv.Atoi(v)
case "h1":
c.H1 = v
case "h2":
c.H2 = v
case "h3":
c.H3 = v
case "h4":
c.H4 = v
case "i1":
c.I1 = v
case "i2":
c.I2 = v
case "i3":
c.I3 = v
case "i4":
c.I4 = v
case "i5":
c.I5 = v
case "name", "remark":
c.Name = v
case "endpoint":
c.Endpoint = v
}
}
if err := c.validate(); err != nil {
return Conf{}, err
}
return c, nil
}
func (c *Conf) validate() error {
if c.PrivateKey == "" {
return fmt.Errorf("awg: нет PrivateKey")
}
if c.PublicKey == "" {
return fmt.Errorf("awg: нет PublicKey")
}
if c.Endpoint == "" {
return fmt.Errorf("awg: нет Endpoint")
}
if len(c.Address) == 0 {
return fmt.Errorf("awg: нет Address")
}
if len(c.AllowedIPs) == 0 {
c.AllowedIPs = []string{"0.0.0.0/0", "::/0"}
}
return nil
}
// HostPort returns UDP endpoint host and port for ping.
func HostPort(raw string) (host, port string, err error) {
c, err := Parse(raw)
if err != nil {
return "", "", err
}
h, p, err := net.SplitHostPort(c.Endpoint)
if err != nil {
// bare host without port
return c.Endpoint, "51820", nil
}
return h, p, nil
}
// ToIPC builds the amneziawg-go UAPI configuration string.
func (c Conf) ToIPC() (string, error) {
priv, err := decodeKey(c.PrivateKey)
if err != nil {
return "", fmt.Errorf("private key: %w", err)
}
pub, err := decodeKey(c.PublicKey)
if err != nil {
return "", fmt.Errorf("public key: %w", err)
}
var b strings.Builder
b.WriteString("private_key=")
b.WriteString(hex.EncodeToString(priv))
writeInt(&b, "jc", c.Jc)
writeInt(&b, "jmin", c.Jmin)
writeInt(&b, "jmax", c.Jmax)
writeInt(&b, "s1", c.S1)
writeInt(&b, "s2", c.S2)
writeInt(&b, "s3", c.S3)
writeInt(&b, "s4", c.S4)
writeStr(&b, "h1", c.H1)
writeStr(&b, "h2", c.H2)
writeStr(&b, "h3", c.H3)
writeStr(&b, "h4", c.H4)
writeStr(&b, "i1", c.I1)
writeStr(&b, "i2", c.I2)
writeStr(&b, "i3", c.I3)
writeStr(&b, "i4", c.I4)
writeStr(&b, "i5", c.I5)
b.WriteString("\npublic_key=")
b.WriteString(hex.EncodeToString(pub))
b.WriteString("\nendpoint=")
b.WriteString(c.Endpoint)
for _, ip := range c.AllowedIPs {
b.WriteString("\nallowed_ip=")
b.WriteString(strings.TrimSpace(ip))
}
if c.PresharedKey != "" {
psk, err := decodeKey(c.PresharedKey)
if err != nil {
return "", fmt.Errorf("preshared key: %w", err)
}
b.WriteString("\npreshared_key=")
b.WriteString(hex.EncodeToString(psk))
}
if c.Keepalive > 0 {
writeInt(&b, "persistent_keepalive_interval", c.Keepalive)
}
return b.String(), nil
}
// LocalAddrs parses Interface Address values.
func (c Conf) LocalAddrs() ([]netip.Addr, error) {
var out []netip.Addr
for _, a := range c.Address {
a = strings.TrimSpace(a)
if a == "" {
continue
}
if p, err := netip.ParsePrefix(a); err == nil {
out = append(out, p.Addr())
continue
}
addr, err := netip.ParseAddr(a)
if err != nil {
return nil, fmt.Errorf("address %q: %w", a, err)
}
out = append(out, addr)
}
if len(out) == 0 {
return nil, fmt.Errorf("no local addresses")
}
return out, nil
}
// DNSAddrs parses DNS servers (defaults to 1.1.1.1).
func (c Conf) DNSAddrs() []netip.Addr {
var out []netip.Addr
for _, d := range c.DNS {
d = strings.TrimSpace(d)
if d == "" {
continue
}
if addr, err := netip.ParseAddr(d); err == nil {
out = append(out, addr)
}
}
if len(out) == 0 {
out = append(out, netip.MustParseAddr("1.1.1.1"))
}
return out
}
func (c Conf) EffectiveMTU() int {
if c.MTU > 0 {
return c.MTU
}
return 1420
}
func decodeKey(s string) ([]byte, error) {
s = strings.TrimSpace(s)
if b, err := base64.StdEncoding.DecodeString(s); err == nil && len(b) == 32 {
return b, nil
}
if b, err := hex.DecodeString(s); err == nil && len(b) == 32 {
return b, nil
}
return nil, fmt.Errorf("expected 32-byte base64/hex key")
}
func writeInt(b *strings.Builder, key string, v int) {
if v == 0 {
return
}
b.WriteByte('\n')
b.WriteString(key)
b.WriteByte('=')
b.WriteString(strconv.Itoa(v))
}
func writeStr(b *strings.Builder, key, v string) {
v = strings.TrimSpace(v)
if v == "" {
return
}
b.WriteByte('\n')
b.WriteString(key)
b.WriteByte('=')
b.WriteString(v)
}
func splitKV(line string) (key, val string, ok bool) {
i := strings.IndexAny(line, "=:")
if i < 0 {
return "", "", false
}
return strings.TrimSpace(line[:i]), strings.TrimSpace(line[i+1:]), true
}
func appendCSV(dst []string, raw string) []string {
for _, p := range strings.Split(raw, ",") {
p = strings.TrimSpace(p)
if p != "" {
dst = append(dst, p)
}
}
return dst
}
func urlDecode(s string) (string, error) {
var b strings.Builder
for i := 0; i < len(s); {
switch s[i] {
case '+':
b.WriteByte(' ')
i++
case '%':
if i+2 >= len(s) {
return s, nil
}
var v byte
for _, c := range []byte{s[i+1], s[i+2]} {
v <<= 4
switch {
case c >= '0' && c <= '9':
v |= c - '0'
case c >= 'a' && c <= 'f':
v |= c - 'a' + 10
case c >= 'A' && c <= 'F':
v |= c - 'A' + 10
default:
return s, nil
}
}
b.WriteByte(v)
i += 3
default:
b.WriteByte(s[i])
i++
}
}
return b.String(), nil
}
// NormalizeShareLink stores a canonical conf body (or passes through awg URI).
func NormalizeShareLink(raw string) (normalized string, remark string, err error) {
c, err := Parse(raw)
if err != nil {
return "", "", err
}
if c.Name != "" {
remark = c.Name
} else if h, _, e := net.SplitHostPort(c.Endpoint); e == nil {
remark = h
} else {
remark = c.Endpoint
}
// Prefer keeping original INI when pasted; re-serialize only for URI imports.
lower := strings.ToLower(strings.TrimSpace(raw))
if strings.HasPrefix(lower, "awg://") || strings.HasPrefix(lower, "amneziawg://") ||
strings.HasPrefix(lower, "wireguard://") {
return serializeINI(c), remark, nil
}
return strings.TrimSpace(raw), remark, nil
}
func serializeINI(c Conf) string {
var b strings.Builder
b.WriteString("[Interface]\n")
b.WriteString("PrivateKey = " + c.PrivateKey + "\n")
if len(c.Address) > 0 {
b.WriteString("Address = " + strings.Join(c.Address, ", ") + "\n")
}
if len(c.DNS) > 0 {
b.WriteString("DNS = " + strings.Join(c.DNS, ", ") + "\n")
}
if c.MTU > 0 {
b.WriteString("MTU = " + strconv.Itoa(c.MTU) + "\n")
}
if c.Jc != 0 {
b.WriteString("Jc = " + strconv.Itoa(c.Jc) + "\n")
}
if c.Jmin != 0 {
b.WriteString("Jmin = " + strconv.Itoa(c.Jmin) + "\n")
}
if c.Jmax != 0 {
b.WriteString("Jmax = " + strconv.Itoa(c.Jmax) + "\n")
}
if c.S1 != 0 {
b.WriteString("S1 = " + strconv.Itoa(c.S1) + "\n")
}
if c.S2 != 0 {
b.WriteString("S2 = " + strconv.Itoa(c.S2) + "\n")
}
if c.S3 != 0 {
b.WriteString("S3 = " + strconv.Itoa(c.S3) + "\n")
}
if c.S4 != 0 {
b.WriteString("S4 = " + strconv.Itoa(c.S4) + "\n")
}
if c.H1 != "" {
b.WriteString("H1 = " + c.H1 + "\n")
}
if c.H2 != "" {
b.WriteString("H2 = " + c.H2 + "\n")
}
if c.H3 != "" {
b.WriteString("H3 = " + c.H3 + "\n")
}
if c.H4 != "" {
b.WriteString("H4 = " + c.H4 + "\n")
}
if c.I1 != "" {
b.WriteString("I1 = " + c.I1 + "\n")
}
if c.I2 != "" {
b.WriteString("I2 = " + c.I2 + "\n")
}
if c.I3 != "" {
b.WriteString("I3 = " + c.I3 + "\n")
}
if c.I4 != "" {
b.WriteString("I4 = " + c.I4 + "\n")
}
if c.I5 != "" {
b.WriteString("I5 = " + c.I5 + "\n")
}
b.WriteString("\n[Peer]\n")
b.WriteString("PublicKey = " + c.PublicKey + "\n")
if c.PresharedKey != "" {
b.WriteString("PresharedKey = " + c.PresharedKey + "\n")
}
b.WriteString("Endpoint = " + c.Endpoint + "\n")
if len(c.AllowedIPs) > 0 {
b.WriteString("AllowedIPs = " + strings.Join(c.AllowedIPs, ", ") + "\n")
}
if c.Keepalive > 0 {
b.WriteString("PersistentKeepalive = " + strconv.Itoa(c.Keepalive) + "\n")
}
return b.String()
}
+73
View File
@@ -0,0 +1,73 @@
package awg
import "testing"
func TestParseINI_AWG20(t *testing.T) {
raw := `[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
Address = 10.8.0.2/32
DNS = 1.1.1.1
Jc = 4
Jmin = 40
Jmax = 70
H1 = 1
H2 = 2
H3 = 3
H4 = 4
I1 = <r 128>
[Peer]
PublicKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Endpoint = 203.0.113.10:51820
AllowedIPs = 0.0.0.0/0
PersistentKeepalive = 25
`
c, err := Parse(raw)
if err != nil {
t.Fatal(err)
}
if c.Jc != 4 || c.I1 != "<r 128>" || c.Endpoint != "203.0.113.10:51820" {
t.Fatalf("unexpected conf: %+v", c)
}
if !c.IsAWG20() {
t.Fatal("expected AWG2 markers")
}
ipc, err := c.ToIPC()
if err != nil {
t.Fatal(err)
}
if !containsAll(ipc, "jc=4", "i1=<r 128>", "endpoint=203.0.113.10:51820") {
t.Fatalf("bad ipc: %s", ipc)
}
}
func TestDetect(t *testing.T) {
if !Detect("[Interface]\nPrivateKey = x") {
t.Fatal("expected detect")
}
if !Detect("awg://1.2.3.4:51820/?private_key=a&public_key=b&address=10.0.0.2/32") {
t.Fatal("expected awg uri detect")
}
}
func containsAll(s string, parts ...string) bool {
for _, p := range parts {
if !stringsContains(s, p) {
return false
}
}
return true
}
func stringsContains(s, sub string) bool {
return len(sub) == 0 || (len(s) >= len(sub) && indexOf(s, sub) >= 0)
}
func indexOf(s, sub string) int {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return i
}
}
return -1
}
+28
View File
@@ -0,0 +1,28 @@
package awg
import "strings"
// Detect is also used by linknorm / netcheck.
func LooksLike(raw string) bool { return Detect(raw) }
// IsAWG20 returns true when obfuscation params typical of AWG 1.5/2.0 are present.
func (c Conf) IsAWG20() bool {
if c.I1 != "" || c.I2 != "" || c.I3 != "" || c.I4 != "" || c.I5 != "" {
return true
}
if c.Jc != 0 || c.Jmin != 0 || c.Jmax != 0 {
return true
}
if c.S1 != 0 || c.S2 != 0 || c.S3 != 0 || c.S4 != 0 {
return true
}
if c.H1 != "" || c.H2 != "" || c.H3 != "" || c.H4 != "" {
return true
}
return false
}
// StripBOM removes a UTF-8 BOM if present.
func StripBOM(s string) string {
return strings.TrimPrefix(s, "\ufeff")
}
+170
View File
@@ -0,0 +1,170 @@
package awg
import (
"context"
"fmt"
"io"
"os"
"sync"
"time"
"github.com/amnezia-vpn/amneziawg-go/conn"
"github.com/amnezia-vpn/amneziawg-go/device"
"github.com/amnezia-vpn/amneziawg-go/tun/netstack"
"vpnclient/internal/config"
)
// Engine runs AmneziaWG 2.0 in userspace (gvisor netstack) and exposes local SOCKS/HTTP.
type Engine struct {
mu sync.Mutex
dev *device.Device
tnet *netstack.Net
proxies *proxyServer
profile config.Profile
stderr io.Writer
running bool
}
func New(stderr io.Writer) *Engine {
if stderr == nil {
stderr = os.Stderr
}
return &Engine{stderr: stderr}
}
func (e *Engine) Protocol() config.Protocol { return config.ProtocolAWG }
func (e *Engine) Start(ctx context.Context, profile config.Profile, _ string) error {
e.mu.Lock()
defer e.mu.Unlock()
if e.running {
return fmt.Errorf("awg: already running")
}
cfg, err := Parse(profile.Proxy)
if err != nil {
return err
}
ipc, err := cfg.ToIPC()
if err != nil {
return err
}
addrs, err := cfg.LocalAddrs()
if err != nil {
return err
}
tunDev, tnet, err := netstack.CreateNetTUN(addrs, cfg.DNSAddrs(), cfg.EffectiveMTU())
if err != nil {
return fmt.Errorf("awg netstack: %w", err)
}
logger := &device.Logger{
Verbosef: func(format string, args ...any) {},
Errorf: func(format string, args ...any) {
fmt.Fprintf(e.stderr, "awg: "+format+"\n", args...)
},
}
dev := device.NewDevice(tunDev, conn.NewDefaultBind(), logger)
if err := dev.IpcSet(ipc); err != nil {
dev.Close()
return fmt.Errorf("awg configure: %w", err)
}
if err := dev.Up(); err != nil {
dev.Close()
return fmt.Errorf("awg up: %w", err)
}
socksHP, _ := profile.SOCKSListenHostPort()
httpHP, _ := profile.HTTPListenHostPort()
if socksHP == "" {
socksHP = "127.0.0.1:1080"
}
if httpHP == "" {
httpHP = "127.0.0.1:1081"
}
proxies, err := startProxies(socksHP, httpHP, tnet.DialContext)
if err != nil {
dev.Close()
return err
}
e.dev = dev
e.tnet = tnet
e.proxies = proxies
e.profile = profile
e.running = true
// Brief handshake grace; don't fail hard if peer is slow.
select {
case <-ctx.Done():
_ = e.stopLocked()
return ctx.Err()
case <-time.After(400 * time.Millisecond):
}
return nil
}
func (e *Engine) Stop() error {
e.mu.Lock()
defer e.mu.Unlock()
return e.stopLocked()
}
func (e *Engine) stopLocked() error {
if !e.running {
return nil
}
if e.proxies != nil {
_ = e.proxies.Close()
e.proxies = nil
}
if e.dev != nil {
e.dev.Close()
e.dev = nil
}
e.tnet = nil
e.running = false
return nil
}
func (e *Engine) Running() bool {
e.mu.Lock()
defer e.mu.Unlock()
return e.running
}
func (e *Engine) LocalHTTPProxy() (string, bool) {
e.mu.Lock()
defer e.mu.Unlock()
if !e.running {
return "", false
}
if e.proxies != nil && e.proxies.httpAddr != "" {
return e.proxies.httpAddr, true
}
return e.profile.HTTPListenHostPort()
}
func (e *Engine) LocalSOCKSProxy() (string, bool) {
e.mu.Lock()
defer e.mu.Unlock()
if !e.running {
return "", false
}
if e.proxies != nil && e.proxies.socksAddr != "" {
return e.proxies.socksAddr, true
}
return e.profile.SOCKSListenHostPort()
}
// EnsureBinary is a no-op: AWG is embedded via amneziawg-go.
func EnsureBinary(_ string) (string, error) {
return "embedded-amneziawg-go", nil
}
// ResolveBinary reports the embedded core.
func ResolveBinary(_ string) (string, error) {
return "embedded-amneziawg-go", nil
}
+245
View File
@@ -0,0 +1,245 @@
package awg
import (
"bufio"
"context"
"encoding/binary"
"fmt"
"io"
"net"
"net/http"
"strings"
"sync"
"time"
)
// DialFunc dials TCP through the AWG userspace stack.
type DialFunc func(ctx context.Context, network, address string) (net.Conn, error)
type proxyServer struct {
mu sync.Mutex
socksLn net.Listener
httpLn net.Listener
dial DialFunc
wg sync.WaitGroup
closed bool
socksAddr string
httpAddr string
}
func startProxies(socksHostPort, httpHostPort string, dial DialFunc) (*proxyServer, error) {
s := &proxyServer{dial: dial}
if socksHostPort != "" {
ln, err := net.Listen("tcp", socksHostPort)
if err != nil {
return nil, fmt.Errorf("awg socks listen %s: %w", socksHostPort, err)
}
s.socksLn = ln
s.socksAddr = ln.Addr().String()
s.wg.Add(1)
go s.serveSOCKS()
}
if httpHostPort != "" {
ln, err := net.Listen("tcp", httpHostPort)
if err != nil {
_ = s.Close()
return nil, fmt.Errorf("awg http listen %s: %w", httpHostPort, err)
}
s.httpLn = ln
s.httpAddr = ln.Addr().String()
s.wg.Add(1)
go s.serveHTTP()
}
return s, nil
}
func (s *proxyServer) Close() error {
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return nil
}
s.closed = true
if s.socksLn != nil {
_ = s.socksLn.Close()
}
if s.httpLn != nil {
_ = s.httpLn.Close()
}
s.mu.Unlock()
s.wg.Wait()
return nil
}
func (s *proxyServer) serveSOCKS() {
defer s.wg.Done()
for {
c, err := s.socksLn.Accept()
if err != nil {
return
}
s.wg.Add(1)
go func(conn net.Conn) {
defer s.wg.Done()
_ = handleSOCKS(conn, s.dial)
}(c)
}
}
func (s *proxyServer) serveHTTP() {
defer s.wg.Done()
for {
c, err := s.httpLn.Accept()
if err != nil {
return
}
s.wg.Add(1)
go func(conn net.Conn) {
defer s.wg.Done()
_ = handleHTTPProxy(conn, s.dial)
}(c)
}
}
func handleSOCKS(client net.Conn, dial DialFunc) error {
defer client.Close()
_ = client.SetDeadline(time.Now().Add(30 * time.Second))
buf := make([]byte, 258)
if _, err := io.ReadFull(client, buf[:2]); err != nil {
return err
}
if buf[0] != 0x05 {
return fmt.Errorf("socks: not v5")
}
nmethods := int(buf[1])
if _, err := io.ReadFull(client, buf[:nmethods]); err != nil {
return err
}
if _, err := client.Write([]byte{0x05, 0x00}); err != nil {
return err
}
if _, err := io.ReadFull(client, buf[:4]); err != nil {
return err
}
if buf[0] != 0x05 || buf[1] != 0x01 {
_, _ = client.Write([]byte{0x05, 0x07, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
return fmt.Errorf("socks: only CONNECT supported")
}
var host string
switch buf[3] {
case 0x01: // IPv4
if _, err := io.ReadFull(client, buf[:4]); err != nil {
return err
}
host = net.IP(buf[:4]).String()
case 0x03: // domain
if _, err := io.ReadFull(client, buf[:1]); err != nil {
return err
}
l := int(buf[0])
if _, err := io.ReadFull(client, buf[:l]); err != nil {
return err
}
host = string(buf[:l])
case 0x04: // IPv6
if _, err := io.ReadFull(client, buf[:16]); err != nil {
return err
}
host = net.IP(buf[:16]).String()
default:
_, _ = client.Write([]byte{0x05, 0x08, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
return fmt.Errorf("socks: bad atyp")
}
if _, err := io.ReadFull(client, buf[:2]); err != nil {
return err
}
port := binary.BigEndian.Uint16(buf[:2])
_ = client.SetDeadline(time.Time{})
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
remote, err := dial(ctx, "tcp", net.JoinHostPort(host, fmt.Sprintf("%d", port)))
if err != nil {
_, _ = client.Write([]byte{0x05, 0x05, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
return err
}
defer remote.Close()
if _, err := client.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0}); err != nil {
return err
}
return relay(client, remote)
}
func handleHTTPProxy(client net.Conn, dial DialFunc) error {
defer client.Close()
_ = client.SetDeadline(time.Now().Add(30 * time.Second))
br := bufio.NewReader(client)
req, err := http.ReadRequest(br)
if err != nil {
return err
}
_ = client.SetDeadline(time.Time{})
if strings.EqualFold(req.Method, http.MethodConnect) {
host := req.Host
if !strings.Contains(host, ":") {
host += ":443"
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
remote, err := dial(ctx, "tcp", host)
if err != nil {
_, _ = client.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
return err
}
defer remote.Close()
if _, err := client.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n")); err != nil {
return err
}
return relay(client, remote)
}
// Absolute-form HTTP proxy request.
u := req.URL
host := u.Host
if host == "" {
host = req.Host
}
if host == "" {
_, _ = client.Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n"))
return fmt.Errorf("http proxy: no host")
}
if !strings.Contains(host, ":") {
host += ":80"
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
remote, err := dial(ctx, "tcp", host)
if err != nil {
_, _ = client.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n"))
return err
}
defer remote.Close()
req.RequestURI = ""
if err := req.Write(remote); err != nil {
return err
}
return relay(client, remote)
}
func relay(a, b net.Conn) error {
errc := make(chan error, 2)
go func() {
_, err := io.Copy(a, b)
errc <- err
}()
go func() {
_, err := io.Copy(b, a)
errc <- err
}()
err := <-errc
_ = a.Close()
_ = b.Close()
<-errc
return err
}