Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.18.0 // indirect
Expand Down Expand Up @@ -70,6 +71,7 @@ require (
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.12.0 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
Expand Down Expand Up @@ -303,6 +305,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
Expand Down
139 changes: 139 additions & 0 deletions internal/proxy/mapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package proxy

import (
"context"
"errors"
"fmt"
"net"
"strconv"

"github.com/huin/goupnp/dcps/internetgateway2"
"golang.org/x/sync/errgroup"
)

// Shamelessly stolen from the guide on the github.
type routerClient interface {
AddPortMapping(
NewRemoteHost string,
NewExternalPort uint16,
NewProtocol string,
NewInternalPort uint16,
NewInternalClient string,
NewEnabled bool,
NewPortMappingDescription string,
NewLeaseDuration uint32,
) (err error)

GetExternalIPAddress() (
NewExternalIPAddress string,
err error,
)
}

type PortMapper struct {
conn routerClient
}

func TryMapper(ctx context.Context) (*PortMapper, error) {
tasks, _ := errgroup.WithContext(ctx)
// Request each type of client in parallel, and return what is found.
var ip1Clients []*internetgateway2.WANIPConnection1
tasks.Go(func() error {
var err error
ip1Clients, _, err = internetgateway2.NewWANIPConnection1ClientsCtx(ctx)
return err
})
var ip2Clients []*internetgateway2.WANIPConnection2
tasks.Go(func() error {
var err error
ip2Clients, _, err = internetgateway2.NewWANIPConnection2ClientsCtx(ctx)
return err
})
var ppp1Clients []*internetgateway2.WANPPPConnection1
tasks.Go(func() error {
var err error
ppp1Clients, _, err = internetgateway2.NewWANPPPConnection1ClientsCtx(ctx)
return err
})

if err := tasks.Wait(); err != nil {
return nil, err
}

var client routerClient
// There are some pretty nasty assumptions here that are most likely correct but not always:
// 1. There is only a single client: this won't be true for some complicated setups
// 2. The server only has a single network interface that has UPnP available.
// The second one becomes an issue during TryMap
switch {
case len(ip2Clients) == 1:
client = ip2Clients[0]
case len(ip1Clients) == 1:
client = ip1Clients[0]
case len(ppp1Clients) == 1:
client = ppp1Clients[0]
default:
return nil, errors.New("multiple or no services found")
}

return &PortMapper{
conn: client,
}, nil
}

// Get the address of the first interface that reports one
// hacky assuming there is only one
func yoinkFirstIntfAddr() (string, error) {
intfs, err := net.Interfaces()
if err != nil {
return "", err
}
for _, intf := range intfs {
addrs, err := intf.Addrs()
if err != nil {
return "", err
}
for _, addr := range addrs {
return addr.String(), nil
}
}

return "", fmt.Errorf("Couldn't enumerate IP from interfaces")
}

func (pm *PortMapper) TryMap(addr net.Addr) error {
host, port, err := net.SplitHostPort(addr.String())
if err != nil {
return err
}

ip := net.ParseIP(host)
if ip == nil {
return fmt.Errorf("Host IP invalid: %s", host)
}

// Check if they bound all interfaces
if ip.IsUnspecified() {
// Assume: only a single network interface
host, err = yoinkFirstIntfAddr()
if err != nil {
return err
}
}

pval, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return fmt.Errorf("Port number parse failed? %v", err)
}

return pm.conn.AddPortMapping(
"",
uint16(pval),
"TCP",
uint16(pval),
host,
true,
"DiscoPanel",
3600,
)
}
11 changes: 10 additions & 1 deletion internal/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Proxy struct {
listenAddr string
running bool
runningMutex sync.RWMutex
portMapper *PortMapper
ctx context.Context
cancel context.CancelFunc
}
Expand Down Expand Up @@ -123,7 +124,15 @@ func (p *Proxy) Start() error {

p.listener = listener
p.running = true

portMapper, err := TryMapper(p.ctx)
if err != nil {
return fmt.Errorf("Failed mapper creation: %w", err)
}
p.portMapper = portMapper
err = p.portMapper.TryMap(p.listener.Addr())
if err != nil {
return fmt.Errorf("Failed try map: %w", err)
}
go p.acceptLoop()

p.logger.Info("Proxy started on %s", p.listenAddr)
Expand Down