Hairpin NAT rule locks out SSH to VyOS router from LAN

I’ve been setting up a fairly recent rolling release build of VyOS 1.4 today and I’ve hopefully gotten it close to what I need. I did run into some issues that I’ve appreciate some help with though. :slight_smile:

  1. First of all, I set up Hairpin NAT according to the official documentation here. However, I realized that my “NAT Reflection: SSH” destination NAT rule (rule 110) for SSH to a home server of mine seems to prevent me from being able to SSH to the VyOS router from the LAN, which kind of makes sense. Is there any way to allow both of these to listen on port 22 and still have both of these use cases work without having to change the VyOS router to listen to SSH on a different port?

That was possible on my current EdgeRouter that the VyOS router is supposed to replace, so I would assume so. I’m just not entirely sure what the best way to accomplish that is. :slight_smile: Can I simply specify the destination address there or something to solve it? Wouldn’t that have to be my WAN IP though?

  1. I tried to add a separate extra management interface that’s assigned a different subnet just in case I get locked out again. However, I’m unable to SSH after setting a static IP in that subnet on my laptop. Clearly there is something simple/obvious that I’m missing here, but I’ve stared too long at the configuration to see it. Any pointers? :smile:

Santized configuration:

firewall {
    flowtable FT-OFFLOAD-v4 {
        interface "eth0"
        interface "eth3"
    }
    group {
        interface-group LAN {
            interface "eth4"
            interface "eth3"
        }
        interface-group MANAGEMENT {
            include "LAN"
            interface "eth8"
        }
        interface-group WAN {
            interface "eth0"
        }
        network-group NET-LAN-v4 {
            network "192.168.10.0/24"
            network "192.168.1.0/24"
        }
    }
    ipv4 {
        forward {
            filter {
                rule 5 {
                    action "offload"
                    offload-target "FT-OFFLOAD-v4"
                    state "established"
                    state "related"
                }
                rule 10 {
                    action "jump"
                    jump-target "CONN_FILTER"
                }
                rule 100 {
                    action "jump"
                    destination {
                        group {
                            network-group "NET-LAN-v4"
                        }
                    }
                    inbound-interface {
                        group "WAN"
                    }
                    jump-target "OUTSIDE-IN"
                }
            }
        }
        input {
            filter {
                default-action "drop"
                rule 10 {
                    action "jump"
                    jump-target "CONN_FILTER"
                }
                rule 20 {
                    action "jump"
                    destination {
                        port "22"
                    }
                    jump-target "VyOS_MANAGEMENT"
                    protocol "tcp"
                }
                rule 30 {
                    action "accept"
                    icmp {
                        type-name "echo-request"
                    }
                    protocol "icmp"
                    state "new"
                }
                rule 40 {
                    action "accept"
                    destination {
                        port "53"
                    }
                    protocol "tcp_udp"
                    source {
                        group {
                            network-group "NET-LAN-v4"
                        }
                    }
                }
                rule 50 {
                    action "accept"
                    source {
                        address "127.0.0.0/8"
                    }
                }
            }
        }
        name CONN_FILTER {
            default-action "return"
            rule 10 {
                action "accept"
                description "Allow established/related"
                state "established"
                state "related"
            }
            rule 20 {
                action "drop"
                description "Drop invalid packets"
                state "invalid"
            }
        }
        name OUTSIDE-IN {
            default-action "drop"
            rule 30 {
                action "accept"
                description "SSH"
                destination {
                    address "192.168.1.10"
                    port "22"
                }
                protocol "tcp"
                state "new"
            }
        }
        name VyOS_MANAGEMENT {
            default-action "return"
            rule 15 {
                action "accept"
                inbound-interface {
                    group "MANAGEMENT"
                }
            }
            rule 20 {
                action "drop"
                inbound-interface {
                    group "WAN"
                }
                state "new"
            }
        }
    }
}
interfaces {
    ethernet eth0 {
        address "dhcp"
        description "WAN (SFP+ 1)"
        hw-id "20:7c:xx:xx:xx:xx"
        offload {
            gro
            gso
        }
        ring-buffer {
            rx "4096"
            tx "4096"
        }
    }
    ethernet eth1 {
        description "SFP+ 2"
        hw-id "20:7c:xx:xx:xx:xx"
    }
    ethernet eth2 {
        description "SFP+ 3"
        hw-id "20:7c:xx:xx:xx:xx"
    }
    ethernet eth3 {
        address "192.168.1.1/24"
        description "LAN (SFP+ 4)"
        hw-id "20:7c:xx:xx:xx:xx"
        offload {
            gro
            gso
        }
        ring-buffer {
            rx "4096"
            tx "4096"
        }
    }
    ethernet eth4 {
        address "192.168.10.1/24"
        description "LAN 2 (RJ45 4)"
        hw-id "20:7c:xx:xx:xx:xx"
        offload {
            gro
            gso
        }
        ring-buffer {
            rx "4096"
            tx "4096"
        }
    }
    ethernet eth5 {
        description "RJ45 2"
        hw-id "20:7c:xx:xx:xx:xx"
    }
    ethernet eth6 {
        description "RJ45 3"
        hw-id "20:7c:xx:xx:xx:xx"
    }
    ethernet eth7 {
        description "RJ45 1"
        hw-id "20:7c:xx:xx:xx:xx"
    }
    ethernet eth8 {
        address "192.168.100.1/24"
        description "Management (RJ45 5)"
        hw-id "20:7c:xx:xx:xx:xx"
    }
    loopback lo {
    }
}
nat {
    destination {
        rule 10 {
            description "SSH"
            destination {
                port "22"
            }
            inbound-interface {
                group "WAN"
            }
            protocol "tcp"
            translation {
                address "192.168.1.10"
            }
        }
        rule 110 {
            description "NAT Reflection: SSH"
            destination {
                port "22"
            }
            inbound-interface {
                group "LAN"
            }
            protocol "tcp"
            translation {
                address "192.168.1.10"
            }
        }
    }
    source {
        rule 100 {
            description "NAT"
            outbound-interface {
                name "eth0"
            }
            source {
                address "192.168.0.0/16"
            }
            translation {
                address "masquerade"
            }
        }
        rule 110 {
            description "NAT Reflection: INSIDE"
            destination {
                address "192.168.1.0/24"
            }
            outbound-interface {
                group "WAN"
            }
            protocol "tcp_udp"
            source {
                address "192.168.1.0/24"
            }
            translation {
                address "masquerade"
            }
        }
    }
}
service {
    dhcp-server {
        shared-network-name LAN {
            subnet 192.168.1.0/24 {
                default-router "192.168.1.1"
                lease "86400"
                name-server "192.168.1.1"
                range 0 {
                    start "192.168.1.10"
                    stop "192.168.1.254"
                }
            }
        }
        shared-network-name LAN2 {
            subnet 192.168.10.0/24 {
                default-router "192.168.10.1"
                lease "86400"
                name-server "192.168.10.1"
                range 0 {
                    start "192.168.10.10"
                    stop "192.168.10.254"
                }
            }
        }
    }
    dns {
        forwarding {
            allow-from "192.168.10.0/24"
            allow-from "192.168.1.0/24"
            cache-size "0"
            listen-address "192.168.10.1"
            listen-address "192.168.1.1"
            name-server 1.0.0.1 {
            }
            name-server 1.1.1.1 {
            }
        }
    }
    ntp {
        allow-client {
            address "0.0.0.0/0"
            address "::/0"
        }
        server time1.vyos.net {
        }
        server time2.vyos.net {
        }
        server time3.vyos.net {
        }
    }
    ssh {
        port "22"
    }
}
system {
    config-management {
        commit-revisions "100"
    }
    conntrack {
        modules {
            ftp
            h323
            nfs
            pptp
            sip
            sqlnet
            tftp
        }
    }
    console {
        device ttyS0 {
            speed "115200"
        }
    }
    host-name "vyos"
    login {
        user xxxxxxxx {
            authentication {
                encrypted-password "xxxxxxxxxx"
            }
        }
    }
    name-server "192.168.10.1"
    name-server "192.168.1.1"
    syslog {
        global {
            facility all {
                level "info"
            }
            facility local7 {
                level "debug"
            }
        }
    }
}

Make your NAT rule 110 more specific, so it no longer matches all destination addresses
set dest IP to WAN IP

1 Like

Won’t that require me to know the dynamically assigned WAN IP ahead of time though?

I wonder how the EdgeRouter is handling this. I tried to look at the iptables rules it has generated from my configuration but didn’t see anything that looked like these extra Hairpin NAT rules I need in VyOS. :thinking:

EDIT: Actually, they do have a guide with very similar rules for when you don’t use their special port-forward commands:

It seems they also do as you suggest. I don’t necessarily know the WAN IP ahead of time though, even though I don’t expect it to change very often.

What would be really convenient is if there was a way to specify the address as “whatever the IP address assigned to interface X is”. :slight_smile: That would probably require support for it in nftables though, to avoid having to reload the firewall rules whenever the IP on that interface changes.

https://vyos.dev/T2196

1 Like

Very promising. Pretty much exactly what I’m looking for. :grinning: The last posts there about potentially destroying the config is a bit of a concern though, so I’ll probably just set the WAN IP manually for now, while I wait for that to be integrated officially.

Thanks for pointing it out so that I can keep track of it though. :grinning:

Would anyone know the answer to my second question? :slight_smile:

Btw. how is documentation handled for rolling builds made after the stable build of a particular version? I noticed a few places where the documentation I followed wasn’t totally accurate due to CLI changes, even though I think I selected sagitta. I would have to double check that though.

If I were to try to submit updates to that documentation, is that valid if I’m on a non-stable release?

  1. If you use dynamic DNS for your WAN IP, you could specify a domain group containing the dynamic FQDN as the destination group in your NAT rule. I have a feeling the above task isn’t likely to be completed anytime soon.

  2. How is that interface connected to the rest of your network? Is anything plugged into it? I ask because it could be down if it’s not plugged in which would make it unreachable. You could also try a dummy interface so nothing is required to be plugged in. Was your laptop plugged directly into it when you tested and could you ping it?

The git repo master branch is for the latest rolling release version, so now 1.5. If you wish to contribute a change specifically to 1.4, make sure you create your working branch from the sagitta branch. Once you have your commit ready on your own fork, create a pull request merging into sagitta branch. Make sure to follow the guidelines at Write Documentation — VyOS 1.4.x (sagitta) documentation and GitHub - vyos/vyos-documentation: VyOS readthedocs. I would suggest just using the Docker image for testing the documentations with the sphinx autobuild command. Thank you for wanting to contribute!

2 Likes
  1. So as long as I have a domain with a DNS record pointing to my WAN IP, I can just create a domain group and set that as the destination? I do, so that could certainly work. What would happen if the router isn’t able to resolve the domain though, for instance when I’m configuring it without providing it an internet connection? Can I just set multiple destinations, one of which is an IP and the other is the domain to make sure there is at least one destination that can be resolved?

  2. I just tried it again for the 5th or 6th time and now it works. I can’t think of what I could’ve changed that could have an impact on it. Oh well, as long as it works I’m happy. :smile: Before, I connected my laptop directly to the interface in question and I’m pretty sure I could ping the router at the IP I set on the interface but I was unable to SSH to it. Now that suddenly works. Must’ve been some firewall change I guess, although I haven’t changed it much besides setting the IP in that DNAT rule.

I see. I’ll look into it. :slight_smile: I’m currently running a custom build of 1.4 with the Intel’s out-of-tree ixgbe driver replacing the in-kernel driver in order to fix the issue with my Intel X553 NICs, as has been discussed in the forum before. I figured it would be closer to a stable release than the 1.5 builds.

If you’re setting up NAT reflection so you can just SSH using the same FQDN all the time, you’re still reliant on being able to resolve the FQDN to begin with. I feel like the answer in this case is split horizon DNS for your LAN if you absolutely must use that FQDN 100% of the time.

I’m just wondering what happens, to make sure it doesn’t become an issue the rare times when it isn’t connected to the internet. Obviously, the intention is that it will have internet 100% of the time once it’s deployed. :slight_smile: Before that happens, it won’t have internet access most of the time however.

EDIT: To clarify, it’s not just for SSH. That’s just the rule that caused issues. I have about 20 DNAT rules (plus the extra rules I need for the roughly 5 I need Hairpin NAT for). I find it useful to be able to quickly look up my WAN IP via DNS locally, so that’s why I don’t want to use split DNS, even though it’s “cleaner”. :slight_smile:

Thanks to all of you who responded. All the posts provided valuable information, but unfortunately I can’t mark more than one post as the solution. :slight_smile:

I ran into a pretty funny issue yesterday when trying out my VyOS box as a router behind my current one just to see that it wasn’t horribly broken (despite ending up with double NAT). :smile:

When I plugged it in, I noticed that most things worked but HTTP and HTTPS didn’t from devices behind the VyOS router. Thankfully I realized fairly quickly that my NAT reflection rules were redirecting all port 80 and 443 connections to my own web server. :smile:

In hindsight, it’s fairly obvious that I should’ve made the same change as I did to the SSH NAT reflection rule to all rules of that type. Maybe this post will help someone else making the same mistake though. :grinning:

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.