Skip to content

Commit 98032c3

Browse files
authored
Improved port definition (#1)
Add support setting external port and protocol per forwarded port Supported syntax: `fwd2me <internal>:<external>:<proto>`
1 parent 8dfda71 commit 98032c3

File tree

4 files changed

+120
-41
lines changed

4 files changed

+120
-41
lines changed

README.md

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,50 @@
1-
## Fwd2Me
1+
# Fwd2Me
22

3-
Fwd2Me - really simple UPnP port forwarding for Linux
3+
Fwd2Me - really simple UPnP port forwarding
44

5-
### Usage
5+
## Usage
66

77
```shell
88
$ fwd2me --help
9-
Usage of fwd2me [options] [port1 port2 ...]:
9+
Usage of fwd2me [options] port1[:remote[:proto]] port2 ...:
1010
-label string
11-
Label for the forwarding (default "fwd2me")
11+
Label for the forwarding (default "fwd2me")
1212
-proto string
13-
Forwarded port protocol (default "TCP")
13+
Default forwarded port protocol (default "TCP")
1414
```
1515
16-
### Example
16+
## Example
17+
18+
### Symmetrical, TCP
1719
1820
```shell
1921
$ fwd2me 80 443
2022
Recreating forwarding from 46.164.xxx.xx to 192.168.1.76
21-
Port 80 forwarded
22-
Port 443 forwarded
23+
Port forwarding created: internal (80), external (80), proto (TCP)
24+
Port forwarding created: internal (443), external (443), proto (TCP)
25+
```
26+
27+
### Symmetrical, UDP
28+
29+
```shell
30+
$ fwd2me -proto UDP 62332
31+
Recreating forwarding from 46.164.xxx.xx to 192.168.1.76
32+
Port forwarding created: internal (62332), external (62332), proto (UDP)
33+
```
34+
35+
### Assymetrical, TCP
36+
37+
```shell
38+
$ fwd2me 16080:80
39+
Recreating forwarding from 46.164.xxx.xx to 192.168.1.76
40+
Port forwarding created: internal (16080), external (80), proto (TCP)
2341
```
2442
43+
44+
### Assymetrical, UDP
45+
46+
```shell
47+
$ fwd2me 65101:51101:UDP
48+
Recreating forwarding from 46.164.xxx.xx to 192.168.1.76
49+
Port forwarding created: internal (65101), external (51101), proto (UDP)
50+
```

ensure.go

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,83 @@ package main
22

33
import (
44
"context"
5+
"errors"
56
"flag"
67
"fmt"
78
"os"
89
"strconv"
10+
"strings"
911
"time"
1012

1113
"github.com/outcatcher/fwd2me/forwarder"
1214
)
1315

16+
const portSeparator = ":"
17+
1418
var (
1519
duration = time.Hour
1620
retryDuration = time.Second
21+
22+
errEmptyPortList = errors.New("empty port list")
1723
)
1824

25+
func parsePort(portStr, defaultProto string) (*forwarder.ForwardedPort, error) {
26+
parts := strings.Split(portStr, portSeparator)
27+
28+
result := &forwarder.ForwardedPort{
29+
Protocol: defaultProto,
30+
}
31+
32+
if len(parts) > 0 {
33+
local, err := strconv.ParseUint(parts[0], 10, 16)
34+
if err != nil {
35+
return nil, fmt.Errorf("failed to parse port, local part %s is not uint16: %w", parts[0], err)
36+
}
37+
38+
result.InternalPort = uint16(local)
39+
result.ExternalPort = result.InternalPort
40+
}
41+
42+
if len(parts) > 1 {
43+
remote, err := strconv.ParseUint(parts[1], 10, 16)
44+
if err != nil {
45+
return nil, fmt.Errorf("failed to parse port, remote part %s is not uint16: %w", parts[1], err)
46+
}
47+
48+
result.ExternalPort = uint16(remote)
49+
}
50+
51+
if len(parts) > 2 {
52+
result.Protocol = parts[2]
53+
}
54+
55+
return result, nil
56+
}
57+
1958
// keepForwarded makes sure ports are forwarded as long as possible without using a long lease.
2059
func keepForwarded(ctx context.Context) error {
2160
portSlice := flag.Args()
2261

23-
if len(portSlice) == 0 {
24-
return fmt.Errorf("empty port list")
25-
}
26-
27-
ports := make([]uint16, 0, len(portSlice))
62+
ports := make([]*forwarder.ForwardedPort, 0, len(portSlice))
2863

2964
for _, portStr := range portSlice {
30-
port, err := strconv.ParseUint(portStr, 10, 16)
65+
forwarded, err := parsePort(portStr, *proto)
3166
if err != nil {
67+
fmt.Fprintln(os.Stderr, "Failed to parse port string:", portStr)
68+
3269
continue
3370
}
3471

35-
ports = append(ports, uint16(port))
72+
ports = append(ports, forwarded)
73+
}
74+
75+
if len(ports) == 0 {
76+
return errEmptyPortList
3677
}
3778

3879
opts := forwarder.ForwardOpts{
3980
LeaseDuration: duration,
40-
Protocol: *proto,
41-
RemoteHost: "",
81+
RemoteHost: "", // default gateway
4282
ProgramName: *label,
4383
Ports: ports,
4484
}

forwarder/forwarder.go

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
"golang.org/x/sync/errgroup"
1616
)
1717

18-
type RouterClient interface {
18+
type routerClient interface {
1919
AddPortMappingCtx(
2020
ctx context.Context,
2121
NewRemoteHost string,
@@ -43,7 +43,7 @@ type RouterClient interface {
4343
LocalAddr() net.IP
4444
}
4545

46-
func pickRouterClient(ctx context.Context) (RouterClient, error) {
46+
func pickRouterClient(ctx context.Context) (routerClient, error) {
4747
tasks, _ := errgroup.WithContext(ctx)
4848
// Request each type of client in parallel, and return what is found.
4949
var ip1Clients []*internetgateway2.WANIPConnection1
@@ -84,23 +84,27 @@ func pickRouterClient(ctx context.Context) (RouterClient, error) {
8484
}
8585
}
8686

87+
type ForwardedPort struct {
88+
InternalPort uint16
89+
ExternalPort uint16
90+
Protocol string
91+
}
92+
8793
type ForwardOpts struct {
8894
RemoteHost string
8995
ProgramName string
90-
Protocol string
91-
Ports []uint16
96+
Ports []*ForwardedPort
9297
LeaseDuration time.Duration
9398
}
9499

95100
type forwardedPort struct {
96-
remoteHost string
101+
remoteHost string // ForwardOpts has no remote host
97102
externalPort uint16
98103
protocol string
99-
endOfLease time.Time
100104
}
101105

102106
type Forwarder struct {
103-
client RouterClient
107+
client routerClient
104108

105109
existingForwarding map[forwardedPort]struct{}
106110
}
@@ -129,24 +133,32 @@ func (f *Forwarder) ForwardPorts(ctx context.Context, opts ForwardOpts) error {
129133

130134
for _, port := range opts.Ports {
131135
// Try to clean up first. That's not optimal, but simplifies workflow.
132-
err := f.client.DeletePortMappingCtx(ctx, opts.RemoteHost, port, opts.Protocol)
136+
err := f.client.DeletePortMappingCtx(ctx, opts.RemoteHost, port.ExternalPort, port.Protocol)
133137
if err != nil {
134138
_, _ = fmt.Fprintln(os.Stderr, "failed to delete before create", err.Error())
135139

136140
errs = errors.Join(errs, err)
137141
}
138142

143+
storedPort := forwardedPort{
144+
remoteHost: opts.RemoteHost,
145+
externalPort: port.ExternalPort,
146+
protocol: port.Protocol,
147+
}
148+
149+
delete(f.existingForwarding, storedPort)
150+
139151
if err := f.client.AddPortMappingCtx(
140152
ctx,
141153
opts.RemoteHost,
142154
// External port number to expose to Internet:
143-
port,
155+
port.ExternalPort,
144156
// Forward TCP (this could be "UDP" if we wanted that instead).
145-
opts.Protocol,
157+
port.Protocol,
146158
// Internal port number on the LAN to forward to.
147159
// Some routers might not support this being different to the external
148160
// port number.
149-
port, // symmetrical
161+
port.InternalPort,
150162
// Internal address on the LAN we want to forward to.
151163
f.client.LocalAddr().String(),
152164
// Enabled:
@@ -158,21 +170,17 @@ func (f *Forwarder) ForwardPorts(ctx context.Context, opts ForwardOpts) error {
158170
// resets, you might want to periodically request before this elapses.
159171
uint32(opts.LeaseDuration.Seconds()),
160172
); err != nil {
161-
extErr := fmt.Errorf("error forwarding port %d: %w", port, err)
173+
extErr := fmt.Errorf("error forwarding port %+v: %w", port, err)
162174

163175
_, _ = fmt.Fprintln(os.Stderr, extErr.Error())
164176

165177
errs = errors.Join(errs, extErr)
166178
}
167179

168-
f.existingForwarding[forwardedPort{
169-
remoteHost: opts.RemoteHost,
170-
externalPort: port,
171-
protocol: opts.Protocol,
172-
endOfLease: time.Now().UTC().Add(opts.LeaseDuration),
173-
}] = struct{}{}
180+
f.existingForwarding[storedPort] = struct{}{}
174181

175-
fmt.Printf("Port %d forwarded\n", port)
182+
fmt.Printf("Port forwarding created: internal (%d), external (%d), proto (%s)\n",
183+
port.InternalPort, port.ExternalPort, port.Protocol)
176184
}
177185

178186
if errs != nil {
@@ -200,7 +208,7 @@ func (f *Forwarder) StopAllForwarding(ctx context.Context) error {
200208

201209
deleted = append(deleted, fwd)
202210

203-
fmt.Printf("Port forwarding for port %d stopped\n", fwd.externalPort)
211+
fmt.Printf("Port forwarding for external port %d stopped\n", fwd.externalPort)
204212
}
205213

206214
for _, deletedPort := range deleted {

main.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"context"
5+
"errors"
56
"flag"
67
"fmt"
78
"os"
@@ -11,12 +12,16 @@ import (
1112

1213
var (
1314
proto = flag.String("proto", "TCP", "Forwarded port protocol")
14-
label = flag.String("label", "fwd2me", "Label for the forwarding")
15+
label = flag.String("label", "fwd2me", "Label for the forwarding shown in router menu")
1516
)
1617

1718
func main() {
1819
flag.Usage = func() {
19-
fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s [options] [port1 port2 ...]:\n", os.Args[0])
20+
fmt.Fprintf(
21+
flag.CommandLine.Output(),
22+
"Usage of %s [options] port1[:external[:proto]] port2 ...:\n",
23+
os.Args[0],
24+
)
2025
flag.PrintDefaults()
2126
}
2227

@@ -33,7 +38,7 @@ func main() {
3338
cancel()
3439
}()
3540

36-
if err := keepForwarded(ctx); err != nil {
41+
if err := keepForwarded(ctx); err != nil && !errors.Is(err, errEmptyPortList) {
3742
_, _ = fmt.Fprintln(os.Stderr, err.Error())
3843

3944
os.Exit(1)

0 commit comments

Comments
 (0)