How (not) to expose services in your home network

Started by meyergru, November 22, 2023, 02:52:40 PM

Previous topic - Next topic
November 22, 2023, 02:52:40 PM Last Edit: February 05, 2024, 05:16:00 PM by meyergru
So now I am fed up with all these discussions about IPv6 and DynDNS shortcomings... beware, this is long!  :P


First, let's talk about IPv4 and how you did it back then.

In the old days (tm) of IPv4, you wanted to expose services on your firewall or your internal machines
(you will later see why this distinction is relevant) to the internet, but alas, your ISP did not give you a static IPv4.

Thus, you set up one or more dynamic DNS name(s) which your router updated via a DynDNS (for "dynamic DNS") provider and open up ports for your services via (D)NAT and be done with it.
Well, you probably had the problem that each service had to use a different port, but you could port-translate
to the destination machine. Thus, every machine could have another port even if the internal service always used the same one.

For IPv4, you also used NAT reflection, such that your clients can use the same external IP (and hence DNS name) from within your own network(s).


But now, it's 2023 and we have IPv6 (and sometimes even only IPv6).


Alas, despite of the abundance of IPv6 addresses, most ISPs only offer dynamic IPv6. So we probably
need DynDNS for IPv6 as well. But many DynDNS providers still do not support IPv6.
Even if they do, we still face problems:


  • Potentially, we want to update both the IPv4 and the IPv6 for a given service. Some DynDNS providers cannot handle both for the same DNS entry and even if they can, you cannot split both updates into two separate requests, because "the last one wins".

  • On the other hand, your firewall probably has no means to specifiy both IPv4 and IPv6 addresses in one request.
  • Protentially, your firewall cannot reliably determine the correct IPv6 anyway, because:


    • It cannot distinguish between routable and non-routable IPv6 addresses.

    • Even if it can, or if you just call an external service or use the WAN IPv6 "implicitly", the routeable IPv6 of the WAN interface is potentially useless, because it is the IA_NA of the firewall itself and not the IA_PD prefix you would like because you really wanted to update the IPv6 for a local client. With IPv6, that distinction becomes vital, because NATv6 is essentially non-existent (and it should be).

Special case: You only have a routeable IPv6, because your ISP only offers CG-NAT for IPv4. Well, forget about the idea of exposing IPv4 services by pure firewall means anyway. You need to rely on a service to translate IPv4 to IPv6 for you or you do it yourself via a cloud VPS.

For what follows, I assume you have both inbound IPv4 and IPv6.

You will have to find ways around these DynDNS problems. We will get to this point later on.

Before that, let us discuss types of services. Roughly, we have these:


  • HTTP or HTTPS, i.e. web-frontends of any kind or some APIs, like the admin UI of a switch, your local installation
    of Unifi controller, a Plex media server or a Proxmox host.
  • TCP-based services like SSH

For type 1, I personally prefer to use HAproxy (There is a great tutorial on this by TheHellSite for OpnSense,
see https://forum.opnsense.org/index.php?topic=23339.0).
Because of termination at the firewall, the main advantages to this are:


  • You can easily use both IPv4 and IPv6 for the same service.
  • You can reach any internal service, regardless if it can handle IPv6 itself.
  • You can translate ports, even with IPv6.
  • You need only one IPv4 and one IPv6, so your DynDNS has to handle all the DNS names, but only two IPs.
  • This is the most important: You can route requests based on DNS names, not via ports or IPs!
  • You can centrally manage certificates and encryption for all of your internal sites.

Notes:

  • c. can be a security plus, because IPv4 port scanners will find it harder to identify services on non-standard ports.
  • e. is more secure as well, because your end services become exposed only to someone also knowing the name of the service (side-note: use a fallback with a fake certificate in order not to expose all of your service names with the real certificate, but be aware of certificate transparency!).
  • f. is obviously more secure because it allows TLS even for services that do not have encryption built-in.
  • Preferably, whenever it is possible, do not expose services at all. For example, I just switched from exposing a centralized Unifi network controller to using it over a Wireguard VPN.


For type 2, you can (and probably should) still use HAproxy, but (mostly) you cannot have e. and f.
You can also expose type 2 services directly, but there are some catches:


  • Obviously, your internal service must be able to handle IPv6 in the first place (advantage b.). If it cannot, but you still do not want to use HAproxy, limit the DynDNS for this client to IPv4 (i.e. do not use a DNS entry that resolves to both IPv4 and IPv6).
  • If you want a service to work over IPv4 and IPv6, it must have the same port over both. Since port translation over IPv6 is difficult at best, you expose the "real" port of the local machine, so you cannot have advantage c. from above.
  • You also lose advantage d. as you must do DynDNS for your local IPv6 that is derived from the IA_PD prefix, not the firewall's IN_NA. This is usually harder.





Now how do we do that type of "dual" DynDNS?

As said earlier, with IPv6, every device gets their own IP, so now the distinction between your firewall and
your other devices becomes relevant (with IPv4, all used the WAN IP).
So, first thing to note: if have have multiple devices, you will need multiple DNS names.

As with IPv4, it is probably your router who does the dynamic DNS updates for you. With IPv6, it can usually do these updates only for itself. There are at least two reasons for this:



  • Most ISPs give you more than one routable IPv6 (globally unique adress or GUA). One is the "network address" (IA_NA for "network address"), which is assigned to the WAN interface of your router and the other one is a prefix (IA_PD for "prefix delegation"), from which you can delegate IPv6 addresses to your local devices.
    This prefix determines between 48 to 64 most-significant bits of the final IPv6 (most commonly, 56 bits are used).

    The rest of the address can be up to 16 bits that are interface-specific (that is, you can assign each internal VLAN a different interface prefix) plus 64 bits of EUI-64, which is derived from the MAC of the client device.


  • Now, where is the problem? The thing to note here is: IA_NA and IA_PD IPv6 ranges are different!
    In order for your firewall to update the dynamic DNS entry for any other local client, it would have to generate  exactly this client's IPv6. But with typical DynDNS update tools like ddclient, all it can usually do is:


    • call an external URL to find its own IPv6 - this would normally be the WAN IPv6, and this is not what we want.
    • find its IPv6 locally - which would be the same in the best case, but wait: since any device and even interface can have multiple IPv6 addresses, which one should it even choose?
    • not use a specific IPv6 at all but rely on the DynDNS provider to detect which IPv6 the request originates from - this is essentially equivalent to variant a, unless you use some trickery.

OpnSense has an (partial) answer to this because it can find either the IPv4 or the IPv6 of a specific interface (via "Check ip methods" "Interface [IPv4]" and "Interface [IPv6]") and provide the result via the macro __MYIP__. This is a "partial solution" because for DynDNS providers that can only accept both IP types in the same request, this will not work without a small trick.


The remedy to these problems is:



  • You can force your firewall to use an IPv6 from the IA_PD range by specifiying "Request only an IPv6 prefix" on the WAN interface. By doing that, your firewall WAN interface does not get a routeable IPv6 (i.e. no IA_NA).
    However, by using "Track Interface" as IPv6 configuration type for your local network interfaces, at least one
    of them will get an IPv6 GUA from the prefix delegation range (IA_PD).
    One of those local interface IPv6s will then be chosen for your firewall's outbound connections. The trick is that the delegated prefix of this IPv6 will be the same for all of your routeable IPv6s. This works even if the client is on another (V)LAN that the outbound (V)LAN interface of your OpnSense.


  • You need a DynDNS provider who can detect the originating IPv6 from the API request plus he is able to cut the most significant N bits from it and have the rest be configured manually for a specific DynDNS name. Preferably, he offers an update API endpoint that is IPv6 only to make sure that the update is done via IPv6 even when "prefer IPv4" is configured.



So, as an example, let us assume there are these devices:



  • Your OpnSense firewall with a MAC of AA:BB:CC:DD:EE:FF, giving an EUI-64 of ::a8bb:ccff:fedd:eeff
  • A LAN server with a LAN MAC of 11:11:11:11:11:11, with an EUI-64 of ::1311:11ff:fe11:1111
  • An IoT device with a VLAN1 MAC of 22:22:22:22:22:22, with an EUI-64 of ::2022:22ff:fe22:2222

Let us further assume that your ISP fives you a /56 IPv6 dynamic public delegation prefix (say dead:beef:feed:fa00::0/56) and you used the 8 network bits as "00" for your LAN and "ce" for your IoT VLAN1 (Be aware that OpnSense wants the network bits in decimal, so this would be 0 and 206, respectively).

In that case, you could configure three DynDNS names like so:




opnsense.dyndns-x.comdead:beef:feed:faXX:a8bb:ccff:fedd:eeff
lanserver.dyndns-x.comdead:beef:feed:fa00:1311:11ff:fe11:1111
iotdevice.dyndns-x.comdead:beef:feed:face:2022:22ff:fe22:2222

Note that you will have to find which interface is being chosen for outbound connections, thus the XX in the OpnSense IPv6.

Whenever your OpnSense gets another dynamic prefix (say cafe:babe:bedd:ab00::0/56), only the first 56 bits on all of these DynDNS entries get updated, because the DynDNS provider uses the new requesting IPv6 (cafe:babe:bedd:abXX:a8bb:ccff:fedd:eeff).

Of course, you have to set up firewall rules to access these devices. But for that, OpnSense has a firewall alias type of "Dynamic IPv6 Host" where you can specifiy the EUI-64 like given above plus the interface to which it is connected. The "Dynamic IPv6 Host" alias will get updated when the IPv6 prefix for the interface changes.

BTW: This assumes you use SLAAC for your internal clients and that they can handle IPv6 at all. For DHCPv6, you would have to use static reservations and use the lower 64 bits for the device instead of the EUI-64. If your clients cannot handle IPv6, many times you could still use HAproxy (there is a tutorial how to set this up) as an application-level gateway between IPv6 and IPv4.


Apart from the shortfalls I gave above, there are several weaknesses in current implementations:



  • ddclient tries to accumulate DynDNS entries (hostname & IP) for the same DynDNS provider into one API request. This is a problem if you want to have both an IPv4 and an IPv6 for the same name in case you create two update rules with different parameters. You can sometimes circumvent that by using different API endpoint names, if available from your DynDNS provider. In OpnSense, there is now a "native" backend which is much better suited than ddclient.

  • Many DynDNS providers handle both IPv4 and IPv6, but the last updates overwrites whatever was there before, so you cannot split updates between IPv4 and IPv6 (e.g. https://www.ddnss.de).

  • At this time, OpnSense cannot determine both IPv4 and IPv6 interface adresses to wrap into one API call (like "...&myip=__MYIPV4__&myip2=__MYIPV6__").

  • Also, OpnSense cannot determine IPv6 addresses "in lieu" of a client, like use their "Dynamic IPv6 Host" firewall alias as a variable (this could not really work like that because the content of that variable can be an array).
    It would be very helpful to directly update DynDNS entries regardless of the API connections's IP protocol and not having to rely on the DynDNS provider to strip something off the IPv6.
    Something like this would even work if the IA_NA of OpnSense was used for the API request.

@AdSchellevis: Fixing weaknesses 3 and 4 would be pure luxury!


Step-by-step instructions for the current, somewhat broken implementation:



  • Register an account at https://dynv6.com


  • Create a zone of your choice, e.g. "testit99.dns.army" and leave the IPv4 and IPv6 fields empty for now


  • Configure your OpnSense to use DHCPv6 on the WAN interface. Specify how many bits your prefix has,  most often it will be 56. Because of another trick introduced later, you can even leave IA_NA on the WAN    interface for outbound connections for this specific DynDNS provider in place.


  • Configure your LAN interface to "Track Interface" WAN for IPv6 and assign an "IPv6 Prefix ID".  Check that it gets an IPv6 GUA from the correct range. If you do this for multiple (V)LANs, find out which of them is being used for outgoing connections by entering "curl v6.ident.me" on the OpnSense CLI. You will see the IPv6 Prefix ID number directly preceeding the EUI-64 bits, so you know the corresponding  outbound (V)LAN interface.
    Take a note of the EUI-64 and preceeding IPv6 Prefix ID number that your OpnSense uses.


  • Optional: If you have a local client / service that you want to address as well, check that Router Advertisements (aka SLAAC)  is configured under "Services->Router Advertisements". Use "Unmanaged" mode and a "Minimum" / "Maximum Interval" of 200 / 600. Check that your client gets an IPv6 GUA as well and also, if it has IPv6 connectivity - if not, check your firewall rules. Take a note of its EUI-64 and which (V)LAN interface it is on.


  • Looking at the DynV6 zone's "Current status", you still have nothing configured for the zone itself.


  • Lookup the Dynv6 HTTP Token in your Dynv6 account settings (under "keys").


  • Set the following under your OpnSense Services->Dynamic DNS->Settings "General settings" tab (enable advanced mode"):

        Enable: checked
        Verbose: checked
        Allow IPv6: checked
        Interval: 300 (or as you like)
        Backend: native

    Apply these settings.


  • Create a new account under your OpnSense Services->Dynamic DNS->Settings:

        Enabled: checked
        Description: whatever you like
        Service: custom
        Protocol: Custom GET
        Server: https://ipv4.dynv6.com/api/update?zone=<YOUR_ZONE_HERE>&token=<YOUR_TOKEN_HERE>&ipv6=__MYIP__/56&ipv4=auto
        Username: x (because technically, you need it)
        Password: x
        Wildcard: unchecked
        Hostname(s): <YOUR_ZONE_HERE>
        Check ip method: Interface [IPv6]
        Interface to monitor: <YOUR_OPNSENSE_OUTBOUND_(V)LAN_INTERFACE>
        Check ip timeout: 10
        Force SSL: checked


    (Note: Yes, we use the IPv4-only API endpoint, which sound weird. That is because we want to have both our
    IPv4 and our IPv6 updated in one go. We can only detect one of these addresses locally and use it via the
    macro "__MYIP__" in the request URI, the other one will be autodetected by the API.
    Also, we want to specify the prefix length (/56) together with the IPv6. Because this cannot be done automatically, it determines which is which. So, we must use IPv4 auto-detection via "ipv4=auto" and for this to work, we have to connect via IPv4 to the API. By using this trick, we could even differentiate between IA_NA and IA_PD, just  because we are able to send the corresponding prefix explicitely, but we would need different zones for this.)

    After that, do not forget to apply the settings. Check the Dynv6 administration interface and verify that your zone now has both an IPv4 and an IPv6 prefix. The IPv6 prefix should have all zeros for the "IPv6 Prefix ID" bits. This enables the use of any VLAN for the final DNS names.


  • Go to the Dynv6 administration console and choose your zone. Switch to the "Records" tab and create a new AAAA record. Leave the name empty and paste the EUI-64 of your chosen outbound OpnSense interface, preceeded by the "IPv6 Prefix ID" bits into the data field and save the entry. It should look something like "::00:a8bb:ccff:fedd:eeff".
    This record will result in the DNS name "testit99.dns.army" from our example being resolvable.
    Note that when you look at the DNS records, they will automatically expand to show the full resulting IPv6 GUA by prepending the zone's IPv6 prefix. You can switch between both views by clicking on the small icon ("expand") above the data column.
    Make sure not to specify the full IPv6, because then it will not be changed by a zone update later on!


  • Optional: If you want your local client(s) to be accessible, create another AAAA record, but this time, assign a name (e.g. "lanserver" from our example, resulting in "lanserver.testit99.dns.army") and  the client's EUI-64 plus prepended "IPv6 Prefix ID" bits for its (V)LAN. If you like, you can also create an additional entry for your OpnSense itself as a subdomain (e.g. "opnsense", resulting in "opnsense.testit99.dns.army").
    You may use multiple zones if you do not like subdomains and prefer "opnsense.dns.army", but you need an    DynDNS update account for each zone. In addition to this, Dynv6 allows you to handle your own, custom DNS domains.


Your OpnSense now has both IPv4 and IPv6 DNS addressability as "testit99.dns.army". Your other LAN client(s) can be addressed via the subdomain name(s) you have created (e.g. "name.testit99.dns.army". You can install IPv6 firewall rules to allow traffic as needed, but be careful.
Intel N100, 4 x I226-V, 16 GByte, 256 GByte NVME, ZTE F6005

1100 down / 800 up, Bufferbloat A+

This is golden!

Regards,
S.
Networking is love. You may hate it, but in the end, you always come back to it.

OPNSense HW
APU2D2 - deceased
N5105 - i226-V | Patriot 2x8G 3200 DDR4 | L 790 512G - VM HA(SOON)
N100   - i226-V | Crucial 16G  4800 DDR5 | S 980 500G - PROD

Why not have your publicly accessible services registered in DynDNS by their current GUA from the systems running these services themselves?

I find proxying/reverse NAT for IPv6 rather fruitless. Getting rid of all that stuff is one of the points of IPv6. You can still have e.g. a crowdsec log collector on each and a central bouncer on the firewall.
Deciso DEC750
People who think they know everything are a great annoyance to those of us who do. (Isaac Asimov)

November 22, 2023, 04:53:16 PM #3 Last Edit: March 31, 2024, 10:20:04 AM by meyergru
Quote from: Patrick M. Hausen on November 22, 2023, 04:29:47 PM
Why not have your publicly accessible services registered in DynDNS by their current GUA from the systems running these services themselves?

I find proxying/reverse NAT for IPv6 rather fruitless. Getting rid of all that stuff is one of the points of IPv6. You can still have e.g. a crowdsec log collector on each and a central bouncer on the firewall.

The point is to offer both IPv4 and IPv6 services, preferably under a unified DNS name / URL. Some mobile providers do not offer IPv6, as well as some company and hotel WLANs. For these cases, IPv6 only won't cut it.

Also, I do not like my devices "polling" randomly for DynDNS updates. The firewall / router knows best when a connection is recreated. Apart from the difficulties with many DynDNS providers and the crude mechanims offered for IPv6 on different platforms, let alone with "dual stack". ddclient is a prominent example of this and @AdSchellevis did a better job at this (although it could be improved to support even more limited DynDNS providers).

As I said, having a name-based solution like HAproxy with all of the centralized management for TLS certificates instead of having to do the same for multiple clients (some of which do not even offer TLS) is a plus for me, too. Therefore, I like @TheHellSite's tutorial. This guide offers the missing step of how to do this for both dynamic IPv4 and IPv6. And you do not have to use proxying/NAT either. The guide explains how IPv6 prefixes can be used to do this for internal IPv6 clients as well.

Intel N100, 4 x I226-V, 16 GByte, 256 GByte NVME, ZTE F6005

1100 down / 800 up, Bufferbloat A+

February 08, 2024, 11:54:00 AM #4 Last Edit: February 08, 2024, 12:03:11 PM by meyergru
I want to add another important warning to this tutorial:

If you aim to hide services behind "names" via HAproxy, do not use single- or multi-domain certificates and also, protect your DNS entries.

Reasoning:

If you are like me, part 8 of TheHellSite's great tutorial may have led you to believe, that you could hide specific potentially vulnerable services behind a name that nobody besides you knows. You may be mistaken...

First off, "security by obscurity" is debatable anyway.

Second off: If the DNS zone of your names allows zone transfers, one could easily find all the names that are assigned. Because with the advent of IPv6, an IP-based scan is not feasible anyway, so an intelligent attacker may take another approach. Namely, he gets the names of sites he want to scan for vulnerabilities and connects to those instead of just randomly hitting at IPs.

So, protect your DNS entries. Either use zones that do not allow zone transfers or use a DNS wildcard in order to not list the names in the first place (this is also easier to manage if you want to add services later on).

Third off: You probably do not know about "certificate transparency". Almost any CA takes part in this, including Letsencrypt. Any certificate that was issued by these CAs is publically shown. For an example, look here and enter your TLD.

Thus, you should never use any single or multi domain certificate but instead use something like "*.dyndns-x.com". Alas, most dyndns providers do not offer this. Probably, they offer "*.yourname.dyndns-x.com", YMMV.

What makes this complicated if that you either have to find a dyndns provider who offers that feature (plus having their DNS zones protected, too!) or use a CNAME from your own DNS zone and have means to control that yourself.

Part of the problem also is, that in order to use wildcard certificates with Letsencrypt, you have to use DNS-01 challenges, forcing you to update the DNS for the zones dynamically. If you do that with BIND, you either must make the whole zone dynamic or sub-delegate _acme-challenge.dyndns-x.com, which is not an easy task in itself and goes way beyond the scope of this tutorial.

What you should have learned by now is that the step-by-step instructions above do not cover this case of wildcard DNS names and therefore are unsafe to use if you aim to have your internal services completely hidden.
Intel N100, 4 x I226-V, 16 GByte, 256 GByte NVME, ZTE F6005

1100 down / 800 up, Bufferbloat A+

One little tidbit: If you use HAproxy with certificates for IMAP access as well, you should know that Dovecot does not handle OCSP stapling currently. So, if you enable OCSP "must staple" in your certificates for dual-use (HTTPS and IMAP/TLS), it will not work. The solution is to separate out the certificate(s) for IMAP, disable OCSP for that and use it for the IMAP frontend only.

I found that the hard way. What was really misleading was that my iPhone worked fine with such a certificate and Dovecot as a server, but Thunderbird did not.
Intel N100, 4 x I226-V, 16 GByte, 256 GByte NVME, ZTE F6005

1100 down / 800 up, Bufferbloat A+