feat(portforward): port redirection with VPN_PORT_FORWARDING_LISTENING_PORT
This commit is contained in:
@@ -51,6 +51,13 @@ func (c *Config) disable(ctx context.Context) (err error) {
|
||||
if err = c.setIPv6AllPolicies(ctx, "ACCEPT"); err != nil {
|
||||
return fmt.Errorf("setting ipv6 policies: %w", err)
|
||||
}
|
||||
|
||||
const remove = true
|
||||
err = c.redirectPorts(ctx, remove)
|
||||
if err != nil {
|
||||
return fmt.Errorf("removing port redirections: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -124,6 +131,11 @@ func (c *Config) enable(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.redirectPorts(ctx, remove)
|
||||
if err != nil {
|
||||
return fmt.Errorf("redirecting ports: %w", err)
|
||||
}
|
||||
|
||||
if err := c.runUserPostRules(ctx, c.customRulesPath, remove); err != nil {
|
||||
return fmt.Errorf("running user defined post firewall rules: %w", err)
|
||||
}
|
||||
@@ -188,3 +200,14 @@ func (c *Config) allowInputPorts(ctx context.Context) (err error) {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) redirectPorts(ctx context.Context, remove bool) (err error) {
|
||||
for _, portRedirection := range c.portRedirections {
|
||||
err = c.redirectPort(ctx, portRedirection.interfaceName, portRedirection.sourcePort,
|
||||
portRedirection.destinationPort, remove)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ type Config struct { //nolint:maligned
|
||||
vpnIntf string
|
||||
outboundSubnets []netip.Prefix
|
||||
allowedInputPorts map[uint16]map[string]struct{} // port to interfaces set mapping
|
||||
portRedirections portRedirections
|
||||
stateMutex sync.Mutex
|
||||
}
|
||||
|
||||
|
||||
@@ -198,6 +198,38 @@ func (c *Config) acceptInputToPort(ctx context.Context, intf string, port uint16
|
||||
})
|
||||
}
|
||||
|
||||
// Used for VPN server side port forwarding, with intf set to the VPN tunnel interface.
|
||||
func (c *Config) redirectPort(ctx context.Context, intf string,
|
||||
sourcePort, destinationPort uint16, remove bool) (err error) {
|
||||
interfaceFlag := "-i " + intf
|
||||
if intf == "*" { // all interfaces
|
||||
interfaceFlag = ""
|
||||
}
|
||||
|
||||
err = c.runIptablesInstructions(ctx, []string{
|
||||
fmt.Sprintf("-t nat %s PREROUTING %s -d 127.0.0.1 -p tcp --dport %d -j REDIRECT --to-ports %d",
|
||||
appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort),
|
||||
fmt.Sprintf("-t nat %s PREROUTING %s -d 127.0.0.1 -p udp --dport %d -j REDIRECT --to-ports %d",
|
||||
appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("redirecting IPv4 source port %d to destination port %d on interface %s: %w",
|
||||
sourcePort, destinationPort, intf, err)
|
||||
}
|
||||
|
||||
err = c.runIP6tablesInstructions(ctx, []string{
|
||||
fmt.Sprintf("-t nat %s PREROUTING %s -d ::1 -p tcp --dport %d -j REDIRECT --to-ports %d",
|
||||
appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort),
|
||||
fmt.Sprintf("-t nat %s PREROUTING %s -d ::1 -p udp --dport %d -j REDIRECT --to-ports %d",
|
||||
appendOrDelete(remove), interfaceFlag, sourcePort, destinationPort),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("redirecting IPv6 source port %d to destination port %d on interface %s: %w",
|
||||
sourcePort, destinationPort, intf, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) runUserPostRules(ctx context.Context, filepath string, remove bool) error {
|
||||
file, err := os.OpenFile(filepath, os.O_RDONLY, 0)
|
||||
if os.IsNotExist(err) {
|
||||
|
||||
119
internal/firewall/redirect.go
Normal file
119
internal/firewall/redirect.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// RedirectPort redirects a source port to a destination port on the interface
|
||||
// intf. If intf is empty, it is set to "*" which means all interfaces.
|
||||
// If a redirection for the source port given already exists, it is removed first.
|
||||
// If the destination port is zero, the redirection for the source port is removed
|
||||
// and no new redirection is added.
|
||||
func (c *Config) RedirectPort(ctx context.Context, intf string, sourcePort,
|
||||
destinationPort uint16) (err error) {
|
||||
c.stateMutex.Lock()
|
||||
defer c.stateMutex.Unlock()
|
||||
|
||||
if sourcePort == 0 {
|
||||
panic("source port cannot be 0")
|
||||
}
|
||||
|
||||
newRedirection := portRedirection{
|
||||
interfaceName: intf,
|
||||
sourcePort: sourcePort,
|
||||
destinationPort: destinationPort,
|
||||
}
|
||||
|
||||
if !c.enabled {
|
||||
c.logger.Info("firewall disabled, only updating redirected ports internal state")
|
||||
if destinationPort == 0 {
|
||||
c.portRedirections.remove(intf, sourcePort)
|
||||
return nil
|
||||
}
|
||||
exists, conflict := c.portRedirections.check(newRedirection)
|
||||
switch {
|
||||
case exists:
|
||||
return nil
|
||||
case conflict != nil:
|
||||
c.portRedirections.remove(conflict.interfaceName,
|
||||
conflict.sourcePort)
|
||||
}
|
||||
c.portRedirections.append(newRedirection)
|
||||
return nil
|
||||
}
|
||||
|
||||
exists, conflict := c.portRedirections.check(newRedirection)
|
||||
switch {
|
||||
case exists:
|
||||
return nil
|
||||
case conflict != nil:
|
||||
const remove = true
|
||||
err = c.redirectPort(ctx, conflict.interfaceName, conflict.sourcePort,
|
||||
conflict.destinationPort, remove)
|
||||
if err != nil {
|
||||
return fmt.Errorf("removing conflicting redirection: %w", err)
|
||||
}
|
||||
c.portRedirections.remove(conflict.interfaceName,
|
||||
conflict.sourcePort)
|
||||
}
|
||||
|
||||
const remove = false
|
||||
err = c.redirectPort(ctx, intf, sourcePort, destinationPort, remove)
|
||||
if err != nil {
|
||||
return fmt.Errorf("redirecting port: %w", err)
|
||||
}
|
||||
c.portRedirections.append(newRedirection)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type portRedirection struct {
|
||||
interfaceName string
|
||||
sourcePort uint16
|
||||
destinationPort uint16
|
||||
}
|
||||
|
||||
type portRedirections []portRedirection
|
||||
|
||||
func (p *portRedirections) remove(intf string, sourcePort uint16) {
|
||||
slice := *p
|
||||
for i, redirection := range slice {
|
||||
interfaceMatch := intf == "" || intf == redirection.interfaceName
|
||||
if redirection.sourcePort == sourcePort && interfaceMatch {
|
||||
// Remove redirection - note: order does not matter
|
||||
slice[i] = slice[len(slice)-1]
|
||||
slice = slice[:len(slice)-1]
|
||||
}
|
||||
}
|
||||
*p = slice
|
||||
}
|
||||
|
||||
func (p *portRedirections) check(dryRun portRedirection) (alreadyExists bool,
|
||||
conflict *portRedirection) {
|
||||
slice := *p
|
||||
for _, redirection := range slice {
|
||||
interfaceMatch := redirection.interfaceName == "" ||
|
||||
redirection.interfaceName == dryRun.interfaceName
|
||||
|
||||
if redirection.sourcePort == dryRun.sourcePort &&
|
||||
redirection.destinationPort == dryRun.destinationPort &&
|
||||
interfaceMatch {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if redirection.sourcePort == dryRun.sourcePort &&
|
||||
interfaceMatch {
|
||||
// Source port has a redirection already for the same interface or all interfaces
|
||||
return false, &redirection
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// append should be called after running `check` to avoid rule conflicts.
|
||||
func (p *portRedirections) append(newRedirection portRedirection) {
|
||||
slice := *p
|
||||
slice = append(slice, newRedirection)
|
||||
*p = slice
|
||||
}
|
||||
Reference in New Issue
Block a user