Netplan: Two NICs, Different IPs On Ubuntu 22.04
Hey guys! Ever found yourself needing to hook up a single Linux machine to two different networks simultaneously, each with its own IP address? Maybe you've got a server that needs to talk to your main internal network and also a separate, isolated network for some specific purpose, like security cameras or a development sandbox. Or perhaps you're setting up a router or a firewall and need distinct interfaces for different traffic flows. Whatever your reason, configuring multiple Network Interface Controllers (NICs) with different IP addresses on Ubuntu using Netplan is totally doable and super handy. Today, we're diving deep into how to nail this setup on Ubuntu 22.04, making sure all your network traffic gets to where it needs to go, smartly returning from the interface it arrived on. Stick around, 'cause we're gonna break it down step-by-step!
Understanding Netplan and Multiple NICs
Alright, let's kick things off by getting a solid grasp on what we're dealing with here. Netplan is the default network configuration tool in modern Ubuntu versions, including our beloved Ubuntu 22.04. It uses YAML files to define network settings, which is pretty slick because YAML is human-readable and less prone to syntax errors than some older config file formats. Before Netplan, we were messing around with /etc/network/interfaces and other scripts, which could get pretty hairy, especially for complex setups. Netplan simplifies this by providing a declarative approach: you declare what you want your network to look like, and Netplan figures out how to achieve it using either systemd-networkd or NetworkManager as its backend. This means we can define static IPs, DHCP, VLANs, bonding, and yes, multiple NICs on separate networks, all within these clean YAML files.
Now, when we talk about configuring two NICs on separate networks, the core idea is that each NIC will have its own IP address and subnet mask, allowing the machine to participate in different network segments independently. A key principle we want to achieve, as mentioned in the setup goal, is ensuring that all traffic returned from the interface it was received on. This is crucial for proper routing and preventing unexpected network behavior. Imagine you have NIC 1 connected to your home LAN (e.g., 192.168.1.x) and NIC 2 connected to a separate IoT network (e.g., 10.0.0.x). Your Ubuntu machine needs to know how to communicate on both. If a device on the 192.168.1.x network sends a packet to our Ubuntu machine's IP on that segment, the reply should go back out through NIC 1. Similarly, if a device on the 10.0.0.x network sends a packet to the Ubuntu machine's IP on that segment, the reply must go out through NIC 2. This is where routing tables and Netplan's configuration come into play. We'll be setting up static IP configurations for both interfaces to ensure this predictable behavior. So, buckle up, because we're about to get our hands dirty with some YAML magic!
Identifying Your Network Interfaces
Before we start typing away at our YAML files, the very first step, guys, is to know exactly which network interfaces Netplan is seeing on your system. Linux can sometimes get a bit creative with interface names β you might have eth0, eth1, ens160, enp3s0, or something even funkier. Using the wrong name in your Netplan config? Instant network failure. Nobody wants that! So, how do we find out what these interfaces are called? It's pretty straightforward. The most common and reliable command for this is ip link show or its older but still functional cousin, ifconfig -a (though ip is the modern standard and generally preferred).
When you run ip link show, you'll see a list of all network interfaces available on your system. Pay attention to the names. You're looking for the physical Ethernet ports. You'll usually see a lo interface, which is your loopback interface (localhost, 127.0.0.1) β don't touch that one! The ones you're interested in will typically start with en (Ethernet) or eth followed by a number, or sometimes more complex names like ens followed by slots and ports. For example, you might see enp0s3 and enp0s8. You can also use lshw -C network to get more detailed hardware information about your network cards, which can sometimes help you visually match them up if you know which port is plugged into which network.
To be absolutely sure, you can even check which interface is currently active and has an IP address assigned using ip a. This command shows your interfaces along with their IP addresses, subnet masks, and states. Look for the interfaces that are UP and have IP addresses. If your machine is already connected to the networks you intend to use, you might be able to identify the interfaces by the IP addresses or network segments they are already communicating with. For instance, if one port is plugged into your router with an IP like 192.168.1.1, the interface connected to that port will likely have an IP in the 192.168.1.x range. Crucially, ensure you have identified the two distinct physical interfaces you intend to configure. If you're unsure, unplugging and replugging a cable and observing which interface status changes (using ip link show or watch -n 1 ip link show) can be a foolproof method. Once you've confidently identified the correct names for your two NICs, you're ready for the next step: crafting that Netplan configuration!
Creating the Netplan Configuration File
Okay, guys, now for the main event: writing the Netplan configuration. Netplan configuration files live in the /etc/netplan/ directory and are typically named with a .yaml extension. Ubuntu usually comes with a default file, often named something like 00-installer-config.yaml or 01-network-manager-all.yaml. It's generally best practice to create a new file for your custom configuration or edit the existing one carefully, rather than just modifying the default one wholesale, especially if you want to keep the installer's defaults intact for other interfaces. Let's say we're creating a new file called 99-dual-nic.yaml. You can create this file using your favorite text editor like nano or vim: sudo nano /etc/netplan/99-dual-nic.yaml.
Inside this file, we'll define the settings for our two network interfaces. Remember the interface names we identified earlier? Let's assume they are enp0s3 and enp0s8. We want to assign static IP addresses to both. A typical Netplan YAML structure looks like this:
network:
version: 2
renderer: networkd
ethernets:
enp0s3:
dhcp4: no
addresses: [192.168.1.10/24]
routes:
- to: default
via: 192.168.1.1
nameservers:
addresses: [8.8.8.8, 8.8.4.4]
enp0s8:
dhcp4: no
addresses: [10.0.0.10/24]
Let's break this down. network: is the top-level key. version: 2 specifies the Netplan API version. renderer: networkd tells Netplan to use systemd-networkd as the backend, which is common for servers and recommended for static configurations. If you were using NetworkManager, you'd specify renderer: NetworkManager.
Under ethernets:, we list each interface we want to configure. So, for enp0s3, we set dhcp4: no because we want a static IP, not DHCP. Then, addresses: [192.168.1.10/24] assigns the IP address 192.168.1.10 with a subnet mask of 255.255.255.0 (represented by /24). This is where you'd put the IP for your first network. The routes: section is important for defining how traffic should leave this interface. to: default means this route applies to all traffic not destined for a directly connected network, and via: 192.168.1.1 specifies the gateway IP address for this network. This gateway should be the router for your 192.168.1.x network. nameservers: defines the DNS servers to use for lookups on this interface.
Now, for the second interface, enp0s8, we do something similar: dhcp4: no for a static IP. addresses: [10.0.0.10/24] assigns it an IP on a different network segment. This is your IP for the second network. Crucially, notice that enp0s8 does not have a default route defined. Why? Because we only want one default route on the entire system, usually associated with the primary network interface. If both interfaces had a default route, the system wouldn't know which one to use for general internet traffic, leading to routing confusion. By not specifying a default route for enp0s8, we ensure that traffic destined for the 10.0.0.x network will be handled by this interface, while general traffic will use the default route specified for enp0s3 (or whichever interface we designate as the primary). This setup guarantees that traffic originating from or destined for the 10.0.0.x network stays within that segment and uses enp0s8, and traffic for the 192.168.1.x network uses enp0s3, fulfilling our requirement of returning traffic from the interface it was received on for its respective network.
Applying the Netplan Configuration
Alright, we've written our fancy YAML file, but it's not doing anything yet! To make Netplan apply our configuration, we need to run a couple of commands. First, it's always a good idea to check the syntax of your YAML file to catch any typos or indentation errors before applying. Netplan has a handy command for this: sudo netplan generate. This command will parse your YAML files and generate the actual network configuration files that systemd-networkd or NetworkManager will use. If there are any syntax errors, it will usually tell you what's wrong. Once you're confident the syntax is correct, the next step is to apply the configuration using sudo netplan apply. This command tells Netplan to enact the changes defined in your YAML files.
When you run sudo netplan apply, Netplan will communicate with the chosen backend (networkd or NetworkManager) to bring up the interfaces with the new settings. You might see some messages indicating that interfaces are being reconfigured. It's important to note that if you're applying this remotely via SSH, there's always a small risk of losing connectivity if something goes wrong. This is why it's often recommended to have physical console access or a backup way to connect (like a serial console) when making significant network changes, especially when configuring multiple interfaces or gateways. However, for this specific setup of assigning static IPs to two interfaces without altering the primary interface's default gateway, the risk is generally lower.
After running sudo netplan apply, you should verify that the changes have taken effect. You can do this by running ip a again. You should now see both enp0s3 and enp0s8 listed, each with its assigned static IP address (192.168.1.10 and 10.0.0.10 in our example). You can also check the routing table with ip route show to confirm that your default route is correctly set for the primary interface (e.g., enp0s3) and that there are routes for the local subnets of both interfaces. To ensure connectivity, try pinging the gateway for each network, for example, ping 192.168.1.1 and ping 10.0.0.1 (assuming these are your gateways). You can also try pinging an external IP address like 8.8.8.8 to test the internet connectivity via your primary interface. Testing is absolutely critical. Don't assume it's working just because the command didn't throw an error. Actually verify that your machine can communicate on both networks as expected. If things aren't working, the first place to look is the syntax of your /etc/netplan/99-dual-nic.yaml file, followed by the output of sudo netplan --debug apply which provides much more verbose logging and can pinpoint issues with the backend service.
Ensuring Traffic Returns Correctly (Policy Routing)
Now, this is where we really nail the requirement: making sure all traffic is returned from the interface it was received on. In our simple Netplan configuration above, we achieved this for the most part by judiciously assigning a default route only to our primary interface (enp0s3). This means any traffic destined for the internet or any network not directly connected to enp0s3 will go through its gateway. Traffic destined for the 10.0.0.x network will naturally be handled by enp0s8 because it's directly connected, and replies will be sent back via enp0s8. Likewise, traffic destined for the 192.168.1.x network will use enp0s3, and replies will go back via enp0s3.
This behavior is generally sufficient for many common use cases. However, sometimes you might need more explicit control, especially if you have complex routing scenarios or want to ensure that traffic originating from a specific IP address always uses a particular outgoing interface, regardless of where it's going. This is where policy routing, also known as advanced routing or source-based routing, comes into play. While our basic Netplan setup handles the return traffic correctly by default for its respective network, policy routing gives you granular control.
For instance, imagine you have multiple default gateways, or you want processes listening on one IP address to only send traffic out of a specific interface. You would typically configure policy routing using the ip rule and ip route commands, often defining separate routing tables. Netplan itself has some limited support for advanced routing, but for very complex scenarios, you might end up creating custom scripts that are run after Netplan applies. For our specific goal, ensuring that traffic returned from an interface goes back out the same interface, the standard Netplan configuration with a single default route is usually enough. The kernel's routing logic is smart: when a packet arrives on enp0s8, the kernel knows enp0s8 is the interface for the 10.0.0.0/24 network. When it needs to send a reply to the source IP of that packet, it consults its routing table. Since the destination IP (the source of the original packet) is on the 10.0.0.x network, the kernel will select enp0s8 as the outgoing interface because it's the most specific match for that destination network. The same logic applies to enp0s3 and the 192.168.1.x network. The key is having distinct subnets for each interface and not configuring multiple default routes unless you know exactly what you're doing.
If you did need advanced policy routing for specific applications or IP addresses, you would configure additional routing tables and rules. For example, you might create a rule that says