Skip to content

Commit 0dcb616

Browse files
initial logic for UDP proxy support
1 parent d6cd160 commit 0dcb616

File tree

5 files changed

+816
-157
lines changed

5 files changed

+816
-157
lines changed

cmd/gohpts/cli.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const usageTproxy string = `
6262
TProxy:
6363
-t Address of transparent proxy server (it starts along with HTTP proxy server)
6464
-T Address of transparent proxy server (no HTTP)
65+
-Tu Address of transparent UDP proxy server
6566
-M Transparent proxy mode: (redirect, tproxy)
6667
-auto Automatically setup iptables for transparent proxy (requires elevated privileges)
6768
-arpspoof Enable ARP spoof proxy for selected targets (Example: "targets 10.0.0.1,10.0.0.5-10,192.168.1.*,192.168.10.0/24;fullduplex false;debug true")
@@ -106,6 +107,7 @@ func root(args []string) error {
106107
if runtime.GOOS == tproxyOS {
107108
flags.StringVar(&conf.TProxy, "t", "", "Address of transparent proxy server (it starts along with HTTP proxy server)")
108109
flags.StringVar(&conf.TProxyOnly, "T", "", "Address of transparent proxy server (no HTTP)")
110+
flags.StringVar(&conf.TProxyUDP, "Tu", "", "Address of transparent UDP proxy server")
109111
flags.Func("M", fmt.Sprintf("Transparent proxy mode: %s", gohpts.SupportedTProxyModes), func(flagValue string) error {
110112
if !slices.Contains(gohpts.SupportedTProxyModes, flagValue) {
111113
fmt.Fprintf(os.Stderr, "%s: %s is not supported (type '%s -h' for help)\n", app, flagValue, app)
@@ -176,19 +178,27 @@ func root(args []string) error {
176178
return fmt.Errorf("transparent proxy mode is not provided: -M flag")
177179
}
178180
}
181+
if seen["Tu"] {
182+
if !seen["M"] {
183+
return fmt.Errorf("transparent proxy mode is not provided: -M flag")
184+
}
185+
if conf.TProxyMode != "tproxy" {
186+
return fmt.Errorf("transparent UDP proxy require tproxy mode")
187+
}
188+
}
179189
if seen["M"] {
180-
if !seen["t"] && !seen["T"] {
181-
return fmt.Errorf("transparent proxy mode requires -t or -T flag")
190+
if !seen["t"] && !seen["T"] && !seen["Tu"] {
191+
return fmt.Errorf("transparent proxy mode requires -t, -T or -Tu flag")
182192
}
183193
}
184194
if seen["auto"] {
185-
if !seen["t"] && !seen["T"] {
186-
return fmt.Errorf("-auto requires -t or -T flag")
195+
if !seen["t"] && !seen["T"] && !seen["Tu"] {
196+
return fmt.Errorf("-auto requires -t, -T or -Tu flag")
187197
}
188198
}
189199
if seen["mark"] {
190-
if !seen["t"] && !seen["T"] {
191-
return fmt.Errorf("-mark requires -t or -T flag")
200+
if !seen["t"] && !seen["T"] && !seen["Tu"] {
201+
return fmt.Errorf("-mark requires -t, -T or -Tu flag")
192202
}
193203
}
194204
if seen["f"] {

gohpts.go

Lines changed: 208 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import (
1313
"fmt"
1414
"io"
1515
"log"
16+
"maps"
1617
"math/rand"
1718
"net"
1819
"net/http"
1920
"os"
21+
"os/exec"
2022
"os/signal"
2123
"runtime"
2224
"slices"
@@ -65,6 +67,7 @@ type Config struct {
6567
ServerConfPath string
6668
TProxy string
6769
TProxyOnly string
70+
TProxyUDP string
6871
TProxyMode string
6972
Auto bool
7073
Mark uint
@@ -135,6 +138,7 @@ type proxyapp struct {
135138
httpServerAddr string
136139
iface *net.Interface
137140
tproxyAddr string
141+
tproxyAddrUDP string
138142
tproxyMode string
139143
auto bool
140144
mark uint
@@ -249,10 +253,11 @@ func New(conf *Config) *proxyapp {
249253
p.logger.Fatal().Msg("Cannot specify TPRoxy and TProxyOnly at the same time")
250254
} else if runtime.GOOS == "linux" && conf.TProxyMode != "" && !slices.Contains(SupportedTProxyModes, conf.TProxyMode) {
251255
p.logger.Fatal().Msg("Incorrect TProxyMode provided")
252-
} else if runtime.GOOS != "linux" && (conf.TProxy != "" || conf.TProxyOnly != "" || conf.TProxyMode != "") {
256+
} else if runtime.GOOS != "linux" && (conf.TProxy != "" || conf.TProxyOnly != "" || conf.TProxyMode != "" || conf.TProxyUDP != "") {
253257
conf.TProxy = ""
254258
conf.TProxyOnly = ""
255259
conf.TProxyMode = ""
260+
conf.TProxyUDP = ""
256261
p.logger.Warn().Msgf("[%s] functionality only available on linux systems", conf.TProxyMode)
257262
}
258263
p.tproxyMode = conf.TProxyMode
@@ -268,6 +273,15 @@ func New(conf *Config) *proxyapp {
268273
if err != nil {
269274
p.logger.Fatal().Err(err).Msg("")
270275
}
276+
if conf.TProxyUDP != "" {
277+
if p.tproxyMode != "tproxy" {
278+
p.logger.Warn().Msgf("[%s] transparent UDP server only supports tproxy mode", conf.TProxyMode)
279+
}
280+
p.tproxyAddrUDP, err = getFullAddress(conf.TProxyUDP, "", true)
281+
if err != nil {
282+
p.logger.Fatal().Err(err).Msg("")
283+
}
284+
}
271285
} else {
272286
p.tproxyAddr, err = getFullAddress(tAddr, "", false)
273287
if err != nil {
@@ -484,6 +498,9 @@ func New(conf *Config) *proxyapp {
484498
p.logger.Info().Msgf("REDIRECT: %s", p.tproxyAddr)
485499
}
486500
}
501+
if p.tproxyAddrUDP != "" {
502+
p.logger.Info().Msgf("TPROXY (UDP): %s", p.tproxyAddrUDP)
503+
}
487504
return &p
488505
}
489506

@@ -496,11 +513,21 @@ func (p *proxyapp) Run() {
496513
go p.arpspoofer.Start()
497514
}
498515
var tproxyServer *tproxyServer
499-
var output map[string]string
516+
opts := make(map[string]string, 5)
517+
if p.auto {
518+
p.applyCommonRedirectRules(opts)
519+
}
500520
if p.tproxyAddr != "" {
501521
tproxyServer = newTproxyServer(p)
502522
if p.auto {
503-
output = tproxyServer.applyRedirectRules()
523+
tproxyServer.applyRedirectRules(opts)
524+
}
525+
}
526+
var tproxyServerUDP *tproxyServerUDP
527+
if p.tproxyAddrUDP != "" {
528+
tproxyServerUDP = newTproxyServerUDP(p)
529+
if p.auto {
530+
tproxyServerUDP.applyRedirectRules(opts)
504531
}
505532
}
506533
if p.proxylist != nil {
@@ -525,15 +552,31 @@ func (p *proxyapp) Run() {
525552
}
526553
close(p.closeConn)
527554
if tproxyServer != nil {
555+
p.logger.Info().Msgf("[tcp %s] Server is shutting down...", p.tproxyMode)
528556
if p.auto {
529-
err := tproxyServer.clearRedirectRules(output)
557+
err := tproxyServer.clearRedirectRules()
530558
if err != nil {
531559
p.logger.Error().Err(err).Msg("Failed clearing iptables rules")
532560
}
533561
}
534-
p.logger.Info().Msgf("[%s] Server is shutting down...", p.tproxyMode)
535562
tproxyServer.Shutdown()
536563
}
564+
if tproxyServerUDP != nil {
565+
p.logger.Info().Msgf("[udp %s] Server is shutting down...", p.tproxyMode)
566+
if p.auto {
567+
err := tproxyServerUDP.clearRedirectRules()
568+
if err != nil {
569+
p.logger.Error().Err(err).Msg("Failed clearing iptables rules")
570+
}
571+
}
572+
tproxyServerUDP.Shutdown()
573+
}
574+
if p.auto {
575+
err := p.clearCommonRedirectRules(opts)
576+
if err != nil {
577+
p.logger.Error().Err(err).Msg("Failed clearing iptables rules")
578+
}
579+
}
537580
p.logger.Info().Msg("Server is shutting down...")
538581
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
539582

@@ -547,6 +590,9 @@ func (p *proxyapp) Run() {
547590
if tproxyServer != nil {
548591
go tproxyServer.ListenAndServe()
549592
}
593+
if tproxyServerUDP != nil {
594+
go tproxyServerUDP.ListenAndServe()
595+
}
550596
if p.user != "" && p.pass != "" {
551597
p.httpServer.Handler = p.proxyAuth(p.handler())
552598
} else {
@@ -571,18 +617,43 @@ func (p *proxyapp) Run() {
571617
p.logger.Error().Err(err).Msg("Failed stopping arp spoofer")
572618
}
573619
}
620+
close(p.closeConn)
621+
if tproxyServer != nil {
622+
p.logger.Info().Msgf("[tcp %s] Server is shutting down...", p.tproxyMode)
623+
if p.auto {
624+
err := tproxyServer.clearRedirectRules()
625+
if err != nil {
626+
p.logger.Error().Err(err).Msg("Failed clearing iptables rules")
627+
}
628+
}
629+
tproxyServer.Shutdown()
630+
}
631+
if tproxyServerUDP != nil {
632+
p.logger.Info().Msgf("[udp %s] Server is shutting down...", p.tproxyMode)
633+
if p.auto {
634+
err := tproxyServerUDP.clearRedirectRules()
635+
if err != nil {
636+
p.logger.Error().Err(err).Msg("Failed clearing iptables rules")
637+
}
638+
}
639+
tproxyServerUDP.Shutdown()
640+
}
574641
if p.auto {
575-
err := tproxyServer.clearRedirectRules(output)
642+
err := p.clearCommonRedirectRules(opts)
576643
if err != nil {
577644
p.logger.Error().Err(err).Msg("Failed clearing iptables rules")
578645
}
579646
}
580-
close(p.closeConn)
581-
p.logger.Info().Msgf("[%s] Server is shutting down...", p.tproxyMode)
582-
tproxyServer.Shutdown()
583647
close(done)
584648
}()
585-
tproxyServer.ListenAndServe()
649+
if tproxyServer != nil && tproxyServerUDP != nil {
650+
go tproxyServerUDP.ListenAndServe()
651+
tproxyServer.ListenAndServe()
652+
} else if tproxyServer != nil {
653+
tproxyServer.ListenAndServe()
654+
} else {
655+
tproxyServerUDP.ListenAndServe()
656+
}
586657
}
587658
<-done
588659
}
@@ -749,7 +820,9 @@ func (p *proxyapp) handleTunnel(w http.ResponseWriter, r *http.Request) {
749820
var dstConn net.Conn
750821
var err error
751822
if network.IsLocalAddress(r.Host) {
752-
dstConn, err = getBaseDialer(timeout, p.mark).Dial("tcp", r.Host)
823+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
824+
defer cancel()
825+
dstConn, err = getBaseDialer(timeout, p.mark).DialContext(ctx, "tcp", r.Host)
753826
if err != nil {
754827
p.logger.Error().Err(err).Msgf("Failed connecting to %s", r.Host)
755828
http.Error(w, err.Error(), http.StatusServiceUnavailable)
@@ -1317,3 +1390,127 @@ func (p *proxyapp) proxyAuth(next http.HandlerFunc) http.HandlerFunc {
13171390
http.Error(w, "Proxy Authentication Required", http.StatusProxyAuthRequired)
13181391
})
13191392
}
1393+
1394+
func (p *proxyapp) applyCommonRedirectRules(opts map[string]string) {
1395+
var setex string
1396+
if p.debug {
1397+
setex = "set -ex"
1398+
}
1399+
if p.tproxyMode == "tproxy" {
1400+
cmdClear := exec.Command("bash", "-c", fmt.Sprintf(`
1401+
%s
1402+
iptables -t mangle -F DIVERT 2>/dev/null || true
1403+
iptables -t mangle -X DIVERT 2>/dev/null || true
1404+
1405+
ip rule del fwmark 1 lookup 100 2>/dev/null || true
1406+
ip route flush table 100 2>/dev/null || true
1407+
`, setex))
1408+
cmdClear.Stdout = os.Stdout
1409+
cmdClear.Stderr = os.Stderr
1410+
if err := cmdClear.Run(); err != nil {
1411+
p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?")
1412+
}
1413+
cmdInit0 := exec.Command("bash", "-c", fmt.Sprintf(`
1414+
%s
1415+
ip rule add fwmark 1 lookup 100 2>/dev/null || true
1416+
ip route add local 0.0.0.0/0 dev lo table 100 2>/dev/null || true
1417+
1418+
iptables -t mangle -N DIVERT 2>/dev/null || true
1419+
iptables -t mangle -F DIVERT 2>/dev/null || true
1420+
iptables -t mangle -A DIVERT -j MARK --set-mark 1
1421+
iptables -t mangle -A DIVERT -j ACCEPT
1422+
`, setex))
1423+
cmdInit0.Stdout = os.Stdout
1424+
cmdInit0.Stderr = os.Stderr
1425+
if err := cmdInit0.Run(); err != nil {
1426+
p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?")
1427+
}
1428+
}
1429+
1430+
_ = createSysctlOptCmd("net.ipv4.ip_forward", "1", setex, opts, p.debug).Run()
1431+
cmdClearForward := exec.Command("bash", "-c", fmt.Sprintf(`
1432+
%s
1433+
iptables -t filter -F GOHPTS 2>/dev/null || true
1434+
iptables -t filter -D FORWARD -j GOHPTS 2>/dev/null || true
1435+
iptables -t filter -X GOHPTS 2>/dev/null || true
1436+
`, setex))
1437+
cmdClearForward.Stdout = os.Stdout
1438+
cmdClearForward.Stderr = os.Stderr
1439+
if err := cmdClearForward.Run(); err != nil {
1440+
p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?")
1441+
}
1442+
var iface *net.Interface
1443+
var err error
1444+
if p.iface != nil {
1445+
iface = p.iface
1446+
} else {
1447+
iface, err = network.GetDefaultInterface()
1448+
if err != nil {
1449+
p.logger.Fatal().Err(err).Msg("failed getting default network interface")
1450+
}
1451+
}
1452+
cmdForwardFilter := exec.Command("bash", "-c", fmt.Sprintf(`
1453+
%s
1454+
iptables -t filter -N GOHPTS 2>/dev/null
1455+
iptables -t filter -F GOHPTS
1456+
iptables -t filter -A FORWARD -j GOHPTS
1457+
iptables -t filter -A GOHPTS -i %s -j ACCEPT
1458+
iptables -t filter -A GOHPTS -o %s -j ACCEPT
1459+
`, setex, iface.Name, iface.Name))
1460+
cmdForwardFilter.Stdout = os.Stdout
1461+
cmdForwardFilter.Stderr = os.Stderr
1462+
if err := cmdForwardFilter.Run(); err != nil {
1463+
p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?")
1464+
}
1465+
}
1466+
1467+
func (p *proxyapp) clearCommonRedirectRules(opts map[string]string) error {
1468+
var setex string
1469+
if p.debug {
1470+
setex = "set -ex"
1471+
}
1472+
cmdClear := exec.Command("bash", "-c", fmt.Sprintf(`
1473+
%s
1474+
iptables -t filter -F GOHPTS 2>/dev/null || true
1475+
iptables -t filter -D FORWARD -j GOHPTS 2>/dev/null || true
1476+
iptables -t filter -X GOHPTS 2>/dev/null || true
1477+
`, setex))
1478+
cmdClear.Stdout = os.Stdout
1479+
cmdClear.Stderr = os.Stderr
1480+
if err := cmdClear.Run(); err != nil {
1481+
p.logger.Fatal().Err(err).Msg("Failed while configuring iptables. Are you root?")
1482+
}
1483+
cmds := make([]string, 0, len(opts))
1484+
for _, cmd := range slices.Sorted(maps.Keys(opts)) {
1485+
cmds = append(cmds, fmt.Sprintf("sysctl -w %s=%s", cmd, opts[cmd]))
1486+
}
1487+
cmdRestoreOpts := exec.Command("bash", "-c", fmt.Sprintf(`
1488+
%s
1489+
%s
1490+
`, setex, strings.Join(cmds, "\n")))
1491+
cmdRestoreOpts.Stdout = os.Stdout
1492+
cmdRestoreOpts.Stderr = os.Stderr
1493+
if !p.debug {
1494+
cmdRestoreOpts.Stdout = nil
1495+
}
1496+
_ = cmdRestoreOpts.Run()
1497+
if p.tproxyMode == "tproxy" {
1498+
cmd := exec.Command("bash", "-c", fmt.Sprintf(`
1499+
%s
1500+
iptables -t mangle -F DIVERT 2>/dev/null || true
1501+
iptables -t mangle -X DIVERT 2>/dev/null || true
1502+
1503+
ip rule del fwmark 1 lookup 100 2>/dev/null || true
1504+
ip route flush table 100 2>/dev/null || true
1505+
`, setex))
1506+
cmd.Stdout = os.Stdout
1507+
cmd.Stderr = os.Stderr
1508+
if !p.debug {
1509+
cmd.Stdout = nil
1510+
}
1511+
if err := cmd.Run(); err != nil {
1512+
return err
1513+
}
1514+
}
1515+
return nil
1516+
}

0 commit comments

Comments
 (0)