When the Kladde is “not reachable”, that can mean a lot: the name isn’t resolved, the address can’t be reached, nobody is listening on the port, a firewall drops the packet, or the app is listening, but in the wrong place. In this lesson you get to know the lower layers: addresses, ports and connections. At the end you design the address plan for the network the Kladde moves into in K4, and have a program check that it is correct.

Addresses: IPv4

Every device in a network has an IP address. An IPv4 address consists of 32 bits, written as four numbers from 0 to 255: 192.168.0.20, 10.20.3.4. Two addresses are special:

  • 127.0.0.1 is called localhost and always means your own machine. Packets to this address never leave it.
  • 0.0.0.0 is not an address you call. A service that binds to 0.0.0.0 listens on all addresses of the machine.

Not every address can be reached on the internet. Three ranges are reserved for internal networks, and they are never routed on the internet:

Range typical use
10.0.0.0/8 company networks, virtual networks in Azure
172.16.0.0/12 internal as well; Docker takes 172.17.0.0/16 from it for its default network
192.168.0.0/16 home networks behind a router

On top of that comes 169.254.0.0/16 for addresses a device gives itself when it doesn’t get one. In Azure, a service answers at 169.254.169.254 that a VM uses, among other things, to fetch the sign-in tokens of its managed identity. That is why you never use this range for a network of your own.

CIDR: networks as a prefix

A network is a block of contiguous addresses. It is written in CIDR notation: the first address, a slash and the number of bits that are the same for all addresses in the block, the prefix. The remaining bits are free. 10.20.0.0/16 means: the first 16 bits (10.20) are fixed, the last 16 are free. That is 2^16 = 65,536 addresses, from 10.20.0.0 to 10.20.255.255.

You calculate the size as 2 to the power of (32 minus the prefix):

Prefix Addresses Example runs from … to
/16 65,536 10.20.0.0/16 10.20.0.0 to 10.20.255.255
/22 1,024 10.20.4.0/22 10.20.4.0 to 10.20.7.255
/23 512 10.20.0.0/23 10.20.0.0 to 10.20.1.255
/24 256 10.20.2.0/24 10.20.2.0 to 10.20.2.255
/26 64 10.20.3.0/26 10.20.3.0 to 10.20.3.63
/32 1 10.20.3.4/32 exactly one address

Two rules that are often forgotten:

  • A block starts at a round spot. A /23 spans two /24 blocks and must start at an even third number: 10.20.0.0/23 and 10.20.2.0/23 exist, 10.20.1.0/23 doesn’t.
  • Two blocks lie either inside each other or side by side. They can’t overlap partially. Whether two blocks overlap, you can therefore see by whether the first address of one lies in the other.

You may know the old notation with a netmask from the network settings of Windows: 255.255.255.0 is the same as /24, 255.255.0.0 the same as /16.

Azure adds a peculiarity: five addresses are reserved in every subnet, the first four and the last one. A /24 there has 251 usable addresses, so, and the smallest subnet is a /29 with three usable ones.

Exercise 1: Calculating

Calculate without a program, on paper or in your head:

  1. How many /24 subnets fit into 10.20.0.0/16?
  2. Which addresses does 10.20.8.0/21 span?
  3. Which of these networks does 10.20.5.17 belong to: 10.20.0.0/23, 10.20.4.0/22 or 10.20.6.0/24?
  4. How many addresses are usable in an Azure subnet 10.20.3.0/26?
  5. Is 10.20.6.0/23 a valid notation? And 10.20.7.0/23?

How to check it: Compare with the solution. If you’re unsure about 2 and 5, write the third number out as eight bits.

Solution 1

  1. 256: a /16 has 8 more free bits than a /24, and 2^8 = 256.
  2. 10.20.8.0 to 10.20.15.255. A /21 has 11 free bits, that is 2,048 addresses or eight /24 blocks: 10.20.8 to 10.20.15.
  3. To 10.20.4.0/22, which runs from 10.20.4.0 to 10.20.7.255. 10.20.0.0/23 ends at 10.20.1.255, 10.20.6.0/24 starts at 10.20.6.0.
  4. 59: 64 addresses minus the five that Azure reserves.
  5. 10.20.6.0/23 yes: 6 is even, the block runs to 10.20.7.255. 10.20.7.0/23 no: 7 is odd, 10.20.7.0 lies in the middle of the block 10.20.6.0/23. As bits: 6 = 00000110, 7 = 00000111. For the /23, the last bit of the third number is free and has to be 0 at the start of the block.

Networks with IPNetwork

.NET has the type System.Net.IPNetwork for this. Parse reads CIDR notation, Contains checks whether an address lies in the network. This program works through an address plan and checks it. Create it in your practice folder as netplan.cs:

C#
// netplan.cs
using System.Buffers.Binary;
using System.Net;

const string VnetCidr = "10.20.0.0/16";
(string Name, string Cidr)[] plan =
[
    ("snet-apps", "10.20.0.0/23"),
    ("snet-endpoints", "10.20.2.0/24"),
    ("AzureBastionSubnet", "10.20.3.0/26"),
];

var vnet = IPNetwork.Parse(VnetCidr);
var subnets = plan.Select(s => (s.Name, s.Cidr, Range: IPNetwork.Parse(s.Cidr))).ToArray();

Console.WriteLine($"{"Network",-20} {"Range",-15} {"From",-13} {"To",-13} {"Size",8} {"Usable",8}");
Console.WriteLine($"{"vnet-kladde-dev",-20} {vnet,-15} {First(vnet),-13} {Last(vnet),-13} {Size(vnet),8}");
foreach (var (name, _, range) in subnets)
{
    Console.WriteLine($"{name,-20} {range,-15} {First(range),-13} {Last(range),-13} {Size(range),8} {Size(range) - 5,8}");
}

var problems = 0;
foreach (var (name, cidr, range) in subnets)
{
    // Parse quietly clears the bits after the prefix: 10.20.1.0/23 becomes 10.20.0.0/23. A plan says what it means.
    if (range.ToString() != cidr)
    {
        Console.WriteLine($"ERROR: {name}: {cidr} doesn't start at the beginning of a block, it reads as {range}.");
        problems++;
    }

    if (!vnet.Contains(range.BaseAddress) || range.PrefixLength < vnet.PrefixLength)
    {
        Console.WriteLine($"ERROR: {name} isn't entirely inside the network {vnet}.");
        problems++;
    }
}

for (var i = 0; i < subnets.Length; i++)
{
    for (var j = i + 1; j < subnets.Length; j++)
    {
        if (Overlaps(subnets[i].Range, subnets[j].Range))
        {
            Console.WriteLine($"ERROR: {subnets[i].Name} and {subnets[j].Name} overlap.");
            problems++;
        }
    }
}

Console.WriteLine(problems == 0 ? "Plan is fine." : $"{problems} errors in the plan.");
return problems == 0 ? 0 : 1;

// Two CIDR blocks either lie one inside the other or apart, so one base address inside the other block is enough.
static bool Overlaps(IPNetwork a, IPNetwork b) => a.Contains(b.BaseAddress) || b.Contains(a.BaseAddress);

static long Size(IPNetwork n) => 1L << (32 - n.PrefixLength);

static IPAddress First(IPNetwork n) => n.BaseAddress;

static IPAddress Last(IPNetwork n)
{
    var number = BinaryPrimitives.ReadUInt32BigEndian(n.BaseAddress.GetAddressBytes()) + (uint)(Size(n) - 1);
    var bytes = new byte[4];
    BinaryPrimitives.WriteUInt32BigEndian(bytes, number);
    return new IPAddress(bytes);
}
Bash
dotnet run netplan.cs
Network              Range           From          To            Size   Usable
vnet-kladde-dev      10.20.0.0/16    10.20.0.0     10.20.255.255    65536
snet-apps            10.20.0.0/23    10.20.0.0     10.20.1.255        512      507
snet-endpoints       10.20.2.0/24    10.20.2.0     10.20.2.255        256      251
AzureBastionSubnet   10.20.3.0/26    10.20.3.0     10.20.3.63          64       59
Plan is fine.

Three places deserve a second look:

  • IPNetwork.Parse is forgiving: from 10.20.1.0/23 it makes 10.20.0.0/23 without an error message. A plan that says 10.20.1.0/23 means something other than what it gets, though. That is why the program compares the block it read with the text that was written.
  • Last calculates the last address as a number: the four bytes of the first address as a 32-bit number in network order (BigEndian), plus size minus one, and back.
  • The program ends with exit code 1 if the plan has errors. That way a script, or later a pipeline, can use it as a check.

Addresses: IPv6

IPv4 has only a little over four billion addresses, and they were handed out long ago. IPv6 has 128 bits, written in eight groups in hexadecimal, such as 2001:db8:12:3::4. Two colons stand for any number of groups of zeros. The counterparts of the special addresses above:

IPv4 IPv6 means
127.0.0.1 ::1 your own machine
0.0.0.0 :: all addresses
10.0.0.0/8 and others fd00::/8 internal, not routed on the internet
169.254.0.0/16 fe80::/10 self-assigned address in the local network

In an address with a port, IPv6 goes in square brackets: http://[::1]:8080/. With ASPNETCORE_HTTP_PORTS=8080, the setting of the .NET images, Kestrel binds to [::]:8080 and accepts IPv4 connections on the same socket too; the log then says Now listening on: http://[::]:8080. Many networks in Azure are still pure IPv4 today. The Kladde’s address plan is too.

Packets, ports and connections

Data travels in packets. Each carries a sender and a destination address. So that a machine knows which program gets a packet, there are ports, numbers from 0 to 65535:

  • The ports up to 1023 are assigned to well-known services: 22 SSH, 25 SMTP, 53 DNS, 80 HTTP, 443 HTTPS. On Linux a program needs special rights to listen on them. That is why ASP.NET Core in a container listens on 8080 (lesson 3.0.1).
  • A program that opens a connection gets a free ephemeral port from the operating system as the sender. On Linux it usually lies between 32768 and 60999, on Windows between 49152 and 65535.

A connection is uniquely identified by five values: protocol, sender address, sender port, destination address, destination port. A socket is the end of such a connection in a program. A service has one socket that listens, and one more for every connection it accepts.

There are two protocols you meet all the time:

TCP UDP
Connection yes, with setup and teardown no, every packet on its own
Order and delivery guaranteed, lost packets are repeated no guarantee
Use HTTP/1.1 and HTTP/2, SQL Server (1433), SMTP, SSH DNS (53), HTTP/3 over QUIC, time synchronization

A TCP connection begins with the three-way handshake:

Client                                   Server (listens on 443)
  | ---- SYN: "I want to connect" -------> |
  | <--- SYN-ACK: "Agreed" ---------------- |
  | ---- ACK: "Let's go" ----------------> |
  |        Connection is up (ESTABLISHED)  |

Only then does data flow, for HTTPS first the TLS handshake (lesson 3.0.4), then the request. Three answers you should learn to tell apart:

  • Connected: Somebody is listening on the port.
  • Refused (connection refused): The machine can be reached, but nobody is listening on this port. It answers at once with a reset.
  • No answer (timeout): The packet doesn’t arrive or the answer doesn’t come back. Usually a firewall silently drops it (lesson 3.0.6), or the address is wrong.

From this difference alone you can often tell whether an error lies with the app or in the network.

127.0.0.1 or 0.0.0.0

A service decides at startup which address it listens on. The following program starts a service on 127.0.0.1 and then one on 0.0.0.0 and tries each time to reach it in two ways: through 127.0.0.1 and through the address the machine has in its network.

C#
// bind.cs
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;

// The machine's own address in its network (not 127.0.0.1): the first IPv4 address of an interface that is up.
var own = NetworkInterface.GetAllNetworkInterfaces()
    .Where(i => i.OperationalStatus == OperationalStatus.Up && i.NetworkInterfaceType != NetworkInterfaceType.Loopback)
    .SelectMany(i => i.GetIPProperties().UnicastAddresses)
    .Select(a => a.Address)
    .First(a => a.AddressFamily == AddressFamily.InterNetwork);

foreach (var bindTo in new[] { IPAddress.Loopback, IPAddress.Any })
{
    using var listener = new TcpListener(bindTo, 5090);
    listener.Start();
    Console.WriteLine($"Service on {listener.LocalEndpoint}");
    foreach (var target in new[] { IPAddress.Loopback, own })
    {
        Console.WriteLine($"  via {target,-15}: {await TryConnectAsync(target, 5090)}");
    }
    listener.Stop();
}

static async Task<string> TryConnectAsync(IPAddress address, int port)
{
    using var client = new TcpClient();
    try
    {
        await client.ConnectAsync(address, port).WaitAsync(TimeSpan.FromSeconds(2));
        return "connected";
    }
    catch (SocketException e)
    {
        return $"refused ({e.SocketErrorCode})";
    }
    catch (TimeoutException)
    {
        return "no answer";
    }
}

In the Codespace:

Bash
dotnet run bind.cs
Service on 127.0.0.1:5090
  via 127.0.0.1      : connected
  via 172.19.80.9    : refused (ConnectionRefused)
Service on 0.0.0.0:5090
  via 127.0.0.1      : connected
  via 172.19.80.9    : connected

The second address will be a different one on your machine. The service on 127.0.0.1 can be reached only through 127.0.0.1. Anyone coming through the address in the network is refused, even from the same machine. On a Windows machine, when the second service starts, Windows Firewall may ask whether dotnet may listen on the network. That is the right question at the right time: allow it for private networks at most, or try the program in the Codespace.

For containers this is the most common network error of all. “From outside” means, for a container: through its network interface eth0, even when the request comes from your own laptop via docker run -p 8080:8080. If the app in the container binds to 127.0.0.1, it listens only to requests from inside the container itself. docker ps still shows the port as published, and from outside only an error comes. Kestrel even warns about it, you just have to read the warning:

warn: Microsoft.AspNetCore.Hosting.Diagnostics[15]
      Overriding HTTP_PORTS '8080' and HTTPS_PORTS ''. Binding to values defined by URLS instead 'http://127.0.0.1:8080'.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://127.0.0.1:8080

Who listens where? ss, Test-NetConnection and curl

On Linux, ss shows the sockets. -t means TCP, -l only listening ones, -n numbers instead of names, -p with program:

Bash
ss -tlnp
State  Recv-Q Send-Q  Local Address:Port  Peer Address:PortProcess
LISTEN 0      512         127.0.0.1:8080       0.0.0.0:*    users:(("dotnet",pid=1,fd=179))

The column Local Address:Port is the answer: 127.0.0.1:8080 means from inside only, 0.0.0.0:8080 from everywhere over IPv4, *:8080 from everywhere over IPv4 and IPv6. There is no ss in the Kladde’s container; you see it with a debug container that shares the Kladde’s network (lesson 3.0.1), after apt-get install -y iproute2. The older netstat -tlnp shows the same, where it is still installed.

On Windows, PowerShell asks the same like this. So that somebody is listening on port 5091, first start shutdown.cs from lesson 3.0.1 in a second terminal with dotnet run shutdown.cs -- --urls http://127.0.0.1:5091 and end it afterwards with Ctrl+C:

PowerShell
Get-NetTCPConnection -State Listen -LocalPort 5091 | Select-Object LocalAddress, LocalPort, State, OwningProcess
LocalAddress LocalPort  State OwningProcess
------------ ---------  ----- -------------
127.0.0.1         5091 Listen        108316

Whether you can reach a port from your machine is checked by Test-NetConnection:

PowerShell
Test-NetConnection -ComputerName 127.0.0.1 -Port 5091
Test-NetConnection -ComputerName 127.0.0.1 -Port 5092
ComputerName     : 127.0.0.1
RemoteAddress    : 127.0.0.1
RemotePort       : 5091
InterfaceAlias   : Loopback Pseudo-Interface 1
SourceAddress    : 127.0.0.1
TcpTestSucceeded : True

WARNING: TCP connect to (127.0.0.1 : 5092) failed
ComputerName           : 127.0.0.1
RemoteAddress          : 127.0.0.1
RemotePort             : 5092
InterfaceAlias         : Loopback Pseudo-Interface 1
SourceAddress          : 127.0.0.1
PingSucceeded          : True
PingReplyDetails (RTT) : 0 ms
TcpTestSucceeded       : False

The number for the process will be different on your machine. The second case is instructive: the machine answers ping (PingSucceeded), but nobody is listening on port 5092. Reachable and ready are two different things. In bash, for the same question you use curl (lesson 3.0.1, exit code 7 for “no connection”) or, for a port without HTTP, nc -vz host port.

ping and traceroute

ping sends ICMP packets, a protocol of its own without ports, and measures how long the answer takes. A missing echo proves little: many machines and almost all services in Azure don’t answer ping at all. A working web app that stays silent on ping is the normal case. For the question “Does my request arrive?”, a test on the port is the better way.

traceroute (tracert on Windows) shows the routers on the way to a destination. Each line is a hop with three measured times:

PowerShell
tracert -d -h 6 learn.microsoft.com
Tracing route to learn.microsoft.com [203.0.113.80]
over a maximum of 6 hops:

  1     1 ms    <1 ms    <1 ms  192.168.178.1
  2     8 ms     7 ms     8 ms  100.71.12.1
  3     9 ms     9 ms     9 ms  198.51.100.17
  4     *        *        *     Request timed out.
  5    11 ms    10 ms    11 ms  198.51.100.45
  6    10 ms    10 ms    10 ms  203.0.113.80

This is an example with made-up addresses from the ranges reserved for documentation. Your hops will be different, and the first is almost always your router. -d leaves out the names of the routers, which is faster. Stars at a hop only mean that this router doesn’t answer; if it goes on after that, everything is fine. In the Codespace you get traceroute after sudo apt-get install -y traceroute; in Container Apps it is part of the debug console.

A packet capture with tcpdump or Wireshark shows every single packet, the handshake from above included. That is the sharpest tool when nothing else explains anything. In K3 you hardly need it, because the telemetry from module 3.4 answers most questions faster.

NAT: many addresses behind one

Your home network has addresses from 192.168.0.0/16, which aren’t routed on the internet. Still, you reach learn.microsoft.com. That is done by NAT (Network Address Translation): on the way out, the router replaces your private sender address and the port with its public address and a port of its own, remembers the mapping and translates the answer back. From outside you see only the router.

You meet NAT again in three places:

  • Docker: docker run -p 8080:8080 forwards port 8080 of your machine to port 8080 of the container. That is NAT in the other direction: from outside to inside.
  • Azure Container Apps: Outgoing connections of the Kladde, for example to the TED interface, reach the recipient from an address of the platform, not from one of your own. If you need a fixed outgoing address, say because a partner allows only that one, you need a network of your own with a NAT gateway (K4).
  • Visitor addresses: Behind NAT, proxies and Front Door, the Kladde doesn’t see the visitor’s address. How to find out anyway, and why you are easily deceived doing so, comes in lesson 3.0.5 and in 3.3.4.

Exercise 2: The Kladde’s address plan

In K4 the Kladde gets a virtual network of its own in Azure: the Container Apps environment lies in it, the database, Key Vault and storage can be reached only through private addresses, and administrative access goes through Azure Bastion. Design the plan for it now.

  1. Choose an address range /16 for the network vnet-kladde-dev. It must not overlap with 172.17.0.0/16 (Docker), with 192.168.0.0/16 (typical home networks that might later be connected through a VPN) and with the ranges that Azure Container Apps reserves for itself (169.254.0.0/16, 172.30.0.0/16, 172.31.0.0/16, 192.0.2.0/24 and, in an environment with workload profiles, 100.100.0.0/17 with three /19 blocks behind it, together 100.100.0.0 to 100.100.223.255).
  2. Carve out three subnets:
    • snet-apps for the Container Apps environment. It requires at least a /27; take room for growth.
    • snet-endpoints for the private endpoints of the database, Key Vault, storage and Service Bus.
    • AzureBastionSubnet for Azure Bastion. The name is prescribed by Azure, the size is at least /26.
  3. Enter your plan into netplan.cs and run the program.
  4. Build in two errors on purpose, an overlap and a block that doesn’t start at a round spot, and see that the program finds them and ends with exit code 1. Take the errors out again.
  5. In the Kladde, create the file docs/network.md: the network, the three subnets with purpose, size and usable addresses, and why you chose the range.

How to check it: dotnet run netplan.cs prints “Plan is fine.”, $? (or $LASTEXITCODE in PowerShell) is 0, with the built-in errors it is 1, and docs/network.md names all three subnets with reasons.

Solution 2

The plan from the example above meets all the conditions. With the two deliberate errors, snet-endpoints at 10.20.1.0/24 and AzureBastionSubnet at 10.20.3.32/26:

Network              Range           From          To            Size   Usable
vnet-kladde-dev      10.20.0.0/16    10.20.0.0     10.20.255.255    65536
snet-apps            10.20.0.0/23    10.20.0.0     10.20.1.255        512      507
snet-endpoints       10.20.1.0/24    10.20.1.0     10.20.1.255        256      251
AzureBastionSubnet   10.20.3.0/26    10.20.3.0     10.20.3.63          64       59
ERROR: AzureBastionSubnet: 10.20.3.32/26 doesn't start at the beginning of a block, it reads as 10.20.3.0/26.
ERROR: snet-apps and snet-endpoints overlap.
2 errors in the plan.

One possible content for docs/network.md:

# The Kladde's network

Basis for the virtual network in K4 (4.7.2). Checked with netplan.cs (exit code 0).

## Address plan dev

| Network            | Range         | Addresses | usable | Purpose                                                 |
| ------------------ | ------------- | --------: | -----: | ------------------------------------------------------- |
| vnet-kladde-dev    | 10.20.0.0/16  |    65,536 |        | the whole network of the dev environment                |
| snet-apps          | 10.20.0.0/23  |       512 |    507 | Container Apps environment, delegated to Microsoft.App/environments |
| snet-endpoints     | 10.20.2.0/24  |       256 |    251 | private endpoints: SQL, Key Vault, Storage, Service Bus |
| AzureBastionSubnet | 10.20.3.0/26  |        64 |     59 | Azure Bastion, name and minimum size prescribed by Azure |

Free for later: 10.20.3.64 to 10.20.255.255.

## Why 10.20.0.0/16

- does not lie in 172.17.0.0/16 (Docker), nor in 192.168.0.0/16 (home networks),
  nor in the ranges that Container Apps reserves;
- test and prod get ranges of their own without overlap (10.21.0.0/16, 10.22.0.0/16),
  so that the networks can be connected later.

Why /23 for the apps, although /27 would do? The size of a subnet can no longer be changed once the environment is in it, and when rolling out a new revision it briefly needs twice as many addresses. There is plenty of room in the /16. For Bastion, on the other hand, /26 is the requirement, and more gains nothing. Whether you take 10.20 or 10.40 makes no difference; what matters is that dev, test and prod never overlap. Networks with the same addresses can’t be connected to each other later, and rebuilding the addresses means creating everything anew.

Exercise 3: Bound to the wrong address

  1. Run bind.cs in the Codespace and explain each of the four lines.
  2. Start the Kladde’s image from K2 once the way it is meant to be, and once with a binding to 127.0.0.1. The commands are below this list. Put the name of your image in place of kladde-web. If the Kladde needs its database for /health, start with docker compose and set the variable there.
  3. After the second start, use a debug container to see where the Kladde listens, and ask it from the inside.
  4. Clean up: docker stop kladde-test.
Bash
docker run --rm -d --name kladde-test -p 8080:8080 kladde-web
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8080/health
docker stop kladde-test
docker run --rm -d --name kladde-test -p 8080:8080 -e ASPNETCORE_URLS=http://127.0.0.1:8080 kladde-web
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8080/health

How to check it: On the first start, curl answers with 200, on the second with an error. In the debug container, ss -tln shows 127.0.0.1:8080, and curl to http://127.0.0.1:8080/health answers 200 there.

Solution 3

On 1: A service on 127.0.0.1 accepts only connections that go to 127.0.0.1. The address in the network belongs to a different interface, and nobody is listening there, so a reset comes at once (ConnectionRefused). A service on 0.0.0.0 listens on all interfaces and can be reached both ways.

On 2: Depending on the Docker version, the second curl reports Empty reply from server (exit code 52) or Connection reset by peer (56), and -w prints 000. Docker’s forwarding does accept the connection on your machine, but in the container it finds nobody who listens on the interface eth0. docker logs kladde-test shows the warning “Overriding HTTP_PORTS” from above.

On 3:

Bash
docker run --rm -it --network container:kladde-test ubuntu:24.04 bash
apt-get update && apt-get install -y --no-install-recommends iproute2 curl
ss -tln
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/health
State  Recv-Q Send-Q  Local Address:Port  Peer Address:PortProcess
LISTEN 0      512         127.0.0.1:8080       0.0.0.0:*
200

From the inside the Kladde is healthy, from the outside it can’t be reached. This is exactly what the error looks like in Azure Container Apps: the platform reports that the app doesn’t answer on its target port, and its health probe fails, although there is not a single error message in the Kladde’s log. The ingress of Container Apps comes, like Docker, from the outside through eth0.

The Kladde after this lesson

  • docs/network.md contains the address plan for vnet-kladde-dev with three subnets and the reasons for the range.
  • The plan is checked with netplan.cs. It is the basis for the Kladde’s network in K4 and for the rules of the network security group in lesson 3.0.6.

Commit docs/network.md as a pull request. netplan.cs stays in your practice folder; if you want to put it next to the plan, use a folder outside the Kladde’s projects.

What you can do now

  • You read and calculate CIDR blocks and know why a block starts at a round spot and how many addresses Azure keeps per subnet.
  • You know the private ranges of IPv4 and the counterparts in IPv6.
  • You tell TCP and UDP apart, know the handshake and tell from “connected”, “refused” and “no answer” where an error lies.
  • You know why a service in a container has to be bound to 0.0.0.0 (or [::]), and you find out with ss, Get-NetTCPConnection and Test-NetConnection who listens where.
  • You know what NAT does and where you meet it in Docker and Azure.
  • The Kladde has a checked address plan.

Further reading