Guide contents

Lesson 10 of 14

HTTP, Ports, and DNS

This lesson covers the commands used to talk to a service, find out what is listening on a machine, and check how a name becomes an address. Every example runs against servers on 127.0.0.1, so a reader can repeat each step without a network.

Command Purpose
curl Send an HTTP request and show or save the response
ss, netstat, lsof -i List listening sockets and the processes that own them
nc Open a raw TCP connection or listen on a port
ip addr, hostname Show the addresses and name of this machine
/etc/hosts Map names to addresses on this machine
dig, nslookup Query a DNS server
ping Check whether a host answers ICMP echo requests

Terms used below. A port is a number from 1 to 65535 that selects a service on one machine. A socket is one end of a network connection. A listening socket waits for incoming connections. A status code is the three-digit number at the start of an HTTP response, such as 200 or 404.

Example 1: a plain GET with curl and the local test servers

The first example starts two local servers and sends the smallest possible request. All later examples reuse these servers.

Save this file as app.py. It answers fixed paths so that every behavior shown later is predictable.

Terminal
mkdir -p demo/site && cd demo
printf '<h1>Hello from the demo server</h1>\n' > site/index.html
printf 'name,qty\nbolt,10\nnut,25\n' > site/report.csv
cat > app.py <<'PY'
import json, sys
from http.server import BaseHTTPRequestHandler, HTTPServer

state = {"flaky": 0}

class H(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"
    server_version = "demo/1.0"
    sys_version = ""
    def log_message(self, *a): pass
    def send(self, code, body, ctype="text/plain", extra=None):
        data = body.encode()
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(data)))
        for k, v in (extra or {}).items():
            self.send_header(k, v)
        self.end_headers()
        if self.command != "HEAD":
            self.wfile.write(data)
    def route(self):
        p = self.path
        if p == "/hello":
            self.send(200, "hello\n")
        elif p == "/old":
            self.send(301, "", extra={"Location": "/hello"})
        elif p == "/boom":
            self.send(500, "server error\n")
        elif p == "/flaky":
            state["flaky"] += 1
            if state["flaky"] < 3:
                self.send(503, "try later\n", extra={"Retry-After": "0"})
            else:
                self.send(200, "ok after %d tries\n" % state["flaky"])
        elif p == "/echo":
            n = int(self.headers.get("Content-Length", 0))
            body = self.rfile.read(n).decode()
            out = {"method": self.command,
                   "content_type": self.headers.get("Content-Type"),
                   "x_token": self.headers.get("X-Token"), "body": body}
            self.send(200, json.dumps(out, sort_keys=True) + "\n", "application/json")
        else:
            self.send(404, "not found\n")
    do_GET = do_POST = do_PUT = do_DELETE = do_HEAD = route

HTTPServer(("127.0.0.1", int(sys.argv[1])), H).serve_forever()
PY
python3 app.py 8081 &
python3 -m http.server 8082 --bind 127.0.0.1 --directory site >/dev/null 2>&1 &
sleep 1
curl http://127.0.0.1:8081/hello
curl -s http://127.0.0.1:8081/hello
Output
hello
hello

python3 app.py 8081 & runs the test server in the background. python3 -m http.server is the standard-library static file server; here it serves the site directory on port 8082. Both bind to 127.0.0.1, the loopback address, so only this machine can reach them.

curl URL sends a GET request and prints the response body. The second command adds -s (silent). Silent mode hides the progress meter and error messages. When curl writes the body to a file or a pipe instead of a terminal, it prints a progress meter on standard error. Scripts use -s to keep that meter out of logs. Add -S (--show-error) to keep error messages while staying silent.

The last section of this lesson stops both servers.

Example 2: -v shows the whole conversation

-v (--verbose) prints the connection steps and both the request and response headers. It is the first option to use when a request does not behave as expected.

Terminal
curl -v http://127.0.0.1:8081/hello
Output
*   Trying 127.0.0.1:8081...
* Connected to 127.0.0.1 (127.0.0.1) port 8081
> GET /hello HTTP/1.1
> Host: 127.0.0.1:8081
> User-Agent: curl/8.5.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Server: demo/1.0 
< Date: Sat, 03 Oct 2026 15:18:56 GMT
< Content-Type: text/plain
< Content-Length: 6
< 
hello
* Connection #0 to host 127.0.0.1 left intact

One extra line about proxy environment variables printed on the machine that produced this output. It is removed here because it depends on the environment. The Date header and the curl version differ between runs and machines.

Prefix Meaning
* Information from curl itself (connection steps)
> A line curl sent (the request)
< A line curl received (the response headers)
none The response body

The request line GET /hello HTTP/1.1 holds the method, the path, and the protocol version. The Host header names the server the client wants. HTTP/1.1 requires it because one address can serve many names. The status line HTTP/1.1 200 OK holds the protocol, the status code, and a reason phrase. Content-Length: 6 is the size of the body in bytes: hello plus a newline.

On a terminal, -v output goes to standard error, so curl -v URL > body.txt saves only the body and prints the diagnostics.

Example 3: -i and -I for headers

-i (--include) prints the response headers followed by the body. -I (--head) sends a HEAD request, which asks the server for the headers only.

Terminal
curl -i http://127.0.0.1:8081/hello
curl -I http://127.0.0.1:8081/hello
Output
HTTP/1.1 200 OK
Server: demo/1.0 
Date: Sat, 03 Oct 2026 15:18:56 GMT
Content-Type: text/plain
Content-Length: 6

hello
HTTP/1.1 200 OK
Server: demo/1.0 
Date: Sat, 03 Oct 2026 15:18:56 GMT
Content-Type: text/plain
Content-Length: 6

The first command prints the headers, a blank line, then hello. The second prints the same headers and no body. A HEAD response carries the Content-Length the body would have, so -I checks the size and type of a large file without downloading it.

Option Method sent Output
none GET Body
-i GET Headers and body
-I HEAD Headers only
-v GET Request and response headers on standard error, body on standard output

Use -I only against servers that implement HEAD. Some servers answer HEAD with a different status than GET.

Example 4: -X, -H, and -d to send a request body

-d (--data) sends a request body and switches the method to POST. -H adds a request header. -X sets the method explicitly. The /echo path of the test server returns what it received as JSON.

Terminal
curl -X POST -H 'X-Token: abc123' -H 'Content-Type: text/plain' -d 'ping' http://127.0.0.1:8081/echo
curl -X DELETE http://127.0.0.1:8081/echo
curl -d 'a=1&b=2' http://127.0.0.1:8081/echo
Output
{"body": "ping", "content_type": "text/plain", "method": "POST", "x_token": "abc123"}
{"body": "", "content_type": null, "method": "DELETE", "x_token": null}
{"body": "a=1&b=2", "content_type": "application/x-www-form-urlencoded", "method": "POST", "x_token": null}
Option Effect
-X METHOD Sets the request method (POST, PUT, DELETE, PATCH)
-H 'Name: value' Adds or replaces a request header; repeat the option for more headers
-d DATA Sends DATA as the body and uses POST unless -X says otherwise
-d @file Reads the body from a file

In the first request, -H 'X-Token: abc123' reached the server as x_token. -H 'Content-Type: text/plain' replaced the default content type. In the third request no header was set, so curl sent application/x-www-form-urlencoded, the type that -d uses by default. The second request shows that -X DELETE sends a different method and an empty body.

-X POST is redundant when -d is present. Combining -X with other flags can change redirect behavior, so use -X only when the method differs from what the other options imply.

Example 5: --json for JSON bodies

--json (curl 7.82 and later) sends the data as the body and sets the Content-Type and Accept headers to application/json. It replaces the three options that every JSON request would otherwise need.

Terminal
curl --json '{"name":"bolt","qty":10}' http://127.0.0.1:8081/echo
curl -s --json '{"name":"bolt"}' http://127.0.0.1:8081/echo | jq .
Output
{"body": "{\"name\":\"bolt\",\"qty\":10}", "content_type": "application/json", "method": "POST", "x_token": null}
{
  "body": "{\"name\":\"bolt\"}",
  "content_type": "application/json",
  "method": "POST",
  "x_token": null
}

The server reports "content_type": "application/json" and "method": "POST" without either being set by hand. The echo server puts the request body inside a string, so the quotes in body are escaped.

The same request without --json is:

Terminal
curl -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' -d '{"name":"bolt"}' http://127.0.0.1:8081/echo

The second command in the example pipes the response to jq, which formats it. -s keeps curl's progress meter out of the pipe. jq is covered in the text processing section of this course. --json @order.json reads the body from a file.

Example 6: -o and -O to save a response

-o FILE writes the body to a named file. -O writes it to a file named after the last part of the URL.

Terminal
mkdir -p dl && cd dl
curl -s -o saved.csv http://127.0.0.1:8082/report.csv
curl -s -O http://127.0.0.1:8082/report.csv
ls
cat saved.csv
curl -O http://127.0.0.1:8082/report.csv
cd ..
Output
report.csv  saved.csv
name,qty
bolt,10
nut,25
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    24  100    24    0     0  22450      0 --:--:-- --:--:-- --:--:-- 24000

The first command saved the body as saved.csv. The second saved it as report.csv, the name at the end of the URL. The last command repeated the download without -s. With -O the body goes to a file, so curl prints the progress meter on the terminal. The speed columns vary between runs.

Option File name
-o NAME NAME, chosen by the caller
-O Last path segment of the URL
-J (with -O) Name from the Content-Disposition response header, when the server sends one
-o /dev/null Discards the body; used when only status or timing matters

Both options overwrite an existing file with the same name. Add --no-clobber (curl 7.83 and later) to keep the existing file and save under a different name. -C - resumes an interrupted download by asking the server for the remaining bytes.

Example 7: -L to follow redirects

A redirect is a response with a status in the 300 range and a Location header that gives another URL. curl stops at the redirect by default.

Terminal
curl -i http://127.0.0.1:8081/old
curl -L http://127.0.0.1:8081/old
curl -sL -o /dev/null -w '%{http_code} %{url_effective} %{num_redirects}\n' http://127.0.0.1:8081/old
Output
HTTP/1.1 301 Moved Permanently
Server: demo/1.0 
Date: Sat, 03 Oct 2026 15:18:56 GMT
Content-Type: text/plain
Content-Length: 0
Location: http://127.0.0.1:8081/hello

hello
200 http://127.0.0.1:8081/hello 1

The first request shows the 301 response with its Location header and an empty body. The second adds -L (--location), so curl sends a second request to /hello and prints that body. On a terminal, curl 8.5 may print the header names in bold and the Location value as a link; the text is the same.

The third command prints facts about the transfer through -w, which Example 8 explains. %{url_effective} is the final URL after all redirects, and %{num_redirects} counts them.

Related options:

Option Effect
-L Follow redirects (up to 50 by default)
--max-redirs N Change the limit
--location-trusted Send credentials to the redirect target too (by default curl drops them when the host changes)

Example 8: -w and -f for scripting

-w FORMAT (--write-out) prints values after the transfer finishes. -f (--fail) makes curl exit with an error on a status of 400 or higher, and it prints no body. Together they let a script decide what to do from the result.

Terminal
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8081/missing
curl -s -o /dev/null -w 'code=%{http_code} size=%{size_download} type=%{content_type}\n' http://127.0.0.1:8081/hello
curl http://127.0.0.1:8081/boom; echo "exit=$?"
curl -f http://127.0.0.1:8081/boom; echo "exit=$?"
curl -sf http://127.0.0.1:8081/boom; echo "exit=$?"
curl -s http://127.0.0.1:9/; echo "exit=$?"
Output
404
code=200 size=6 type=text/plain
server error
exit=0
curl: (22) The requested URL returned error: 500
exit=22
exit=22
exit=7

The first two commands send the body to /dev/null and print only the format string. Common variables:

Variable Value
%{http_code} Status code of the final response
%{size_download} Bytes of body received
%{content_type} Value of the Content-Type header
%{url_effective} Final URL after redirects
%{time_total} Total transfer time in seconds (varies between runs)
%{remote_ip} Address of the server that answered

The third command shows the default behavior: a 500 response is a successful transfer from curl's point of view, so the body prints and the exit status is 0. The fourth adds -f: curl prints curl: (22) The requested URL returned error: 500 and exits with 22. The fifth adds -s, which hides that message, so only the exit status 22 remains. The last command targets port 9, where nothing listens. curl exits with 7, the code for "failed to connect".

A script can branch on these codes:

Terminal
if curl -sf -o /dev/null http://127.0.0.1:8081/hello; then echo up; else echo down; fi

-sS with -f prints the error message but nothing else, which suits a cron job that mails its output.

Example 9: --retry for flaky servers

--retry N repeats a request up to N times when it fails with a transient error. curl treats timeouts and the status codes 408, 429, 500, 502, 503, and 504 as transient. The /flaky path of the test server answers 503 twice and then succeeds.

Terminal
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8081/boom
curl --retry 3 --retry-delay 0 -s -w ' [%{http_code}]\n' http://127.0.0.1:8081/flaky
curl --retry 3 --retry-delay 0 -s -w ' [%{http_code}]\n' http://127.0.0.1:8081/flaky
Output
500
try later
try later
ok after 3 tries
 [200]
ok after 4 tries
 [200]

The first command shows that /boom answers 500. The second request reached the server three times. The first two answers (try later) were 503, so curl retried; the third succeeded, and -w printed the final status [200]. The bodies of the 503 responses printed because curl shows the body of every response it receives. The third command ran against the same server process, which had already counted three requests. The counter continued to 4, so that request succeeded on the first try.

Option Effect
--retry N Retry up to N times on transient errors
--retry-delay S Wait S seconds between tries; without it, the wait starts at 1 second and doubles each time
--retry-max-time S Stop retrying after S seconds in total
--retry-connrefused Also retry when the connection is refused
--retry-all-errors Retry on any error (curl 7.71 and later)

--retry does not retry a plain 404, because that status is not transient. A server that is still starting often refuses connections, so deployment scripts use --retry-connrefused.

Example 10: ss, netstat, and lsof for listening ports

Three commands list sockets. ss is the current Linux tool and part of the iproute2 package. netstat is the older tool from net-tools. lsof lists open files, and a network socket is an open file.

ss

Terminal
ss -tlnp
ss -tln 'sport = :8081'
Output
State  Recv-Q Send-Q Local Address:Port  Peer Address:PortProcess                           
LISTEN 0      5          127.0.0.1:8082       0.0.0.0:*    users:(("python3",pid=7316,fd=3))
LISTEN 0      5          127.0.0.1:8081       0.0.0.0:*    users:(("python3",pid=7315,fd=3))
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      5          127.0.0.1:8081      0.0.0.0:*          

The first command printed every listening TCP socket on the machine that ran it. Only the two lines for the test servers are kept here. The process ids change on every run.

Flag Meaning
-t TCP sockets
-u UDP sockets
-l Listening sockets only
-n Numbers for addresses and ports, with no name lookups
-p The process that owns each socket (other users' sockets need root)
-a All sockets, listening and connected

Local Address:Port of 127.0.0.1:8081 means the server accepts connections only from this machine. An address of 0.0.0.0 or * means every IPv4 interface. [::] means every IPv6 interface. This column explains most "connection refused from another machine" problems: a server bound to 127.0.0.1 is unreachable from outside. On a listening socket, Send-Q is the size of the accept backlog, here 5, and Recv-Q counts connections waiting to be accepted. The second command filters by source port with an ss expression.

netstat

Terminal
netstat -tlnp
Output
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.1:8082          0.0.0.0:*               LISTEN      7316/python3        
tcp        0      0 127.0.0.1:8081          0.0.0.0:*               LISTEN      7315/python3        

The flags -tlnp mean the same as in ss. netstat reads /proc/net files and is slower on machines with many connections, which is why ss replaced it on Linux. Many systems no longer install it by default.

lsof

Terminal
lsof -nP -iTCP:8081 -sTCP:LISTEN
lsof -nP -i :8082
Output
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
python3 7315 root    3u  IPv4  21579      0t0  TCP 127.0.0.1:8081 (LISTEN)
COMMAND  PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
python3 7316 root    3u  IPv4  22735      0t0  TCP 127.0.0.1:8082 (LISTEN)

-i selects network files, and -i :8082 selects one port. -iTCP:8081 limits it to TCP, and -sTCP:LISTEN keeps only listening sockets. -n skips host name lookups and -P skips port name lookups, the same purpose as -n in ss. The answer to "which process holds port 8081" is the COMMAND and PID columns.

Task Linux macOS
Listening TCP ports ss -tlnp lsof -nP -iTCP -sTCP:LISTEN
Who uses port N ss -tlnp 'sport = :N' or lsof -i :N lsof -nP -i :N
netstat -p selects the process -p selects a protocol; process columns come from lsof instead

macOS has no ss. The macOS netstat accepts different flags, so the Linux command netstat -tlnp does not carry over.

Example 11: nc to listen and connect

nc (netcat) opens raw TCP connections. It sends what it reads from standard input and prints what it receives. The examples use the OpenBSD variant, which is the default on Debian and Ubuntu. Flags differ in other variants (see the table below).

Start a listener that writes what it receives to a file, check the port, and connect to it:

Terminal
(nc -l 127.0.0.1 9000 > got.txt &); sleep 0.5
ss -tln 'sport = :9000'
printf 'hello over tcp\n' | nc -N 127.0.0.1 9000; sleep 0.5
cat got.txt
Output
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      1          127.0.0.1:9000      0.0.0.0:*          
hello over tcp

nc -l 127.0.0.1 9000 listens on port 9000. ss confirms that the socket exists. The second nc connects and sends one line. -N closes the connection after standard input ends; without it the OpenBSD variant keeps the connection open and the command waits. The line arrived in got.txt. The listener accepts one connection and then exits, so the port is closed again afterwards.

nc as a fake HTTP server

A listener can answer with a hand-written HTTP response. This shows what a client sends, because nc records the raw request.

Terminal
(printf 'HTTP/1.1 200 OK\r\nContent-Length: 3\r\nConnection: close\r\n\r\nhi\n' | nc -l 127.0.0.1 9000 > req.txt &); sleep 0.5
curl -s -i http://127.0.0.1:9000/from-curl -H 'X-Demo: 1'; sleep 0.5
echo ---; cat req.txt
Output
HTTP/1.1 200 OK
Content-Length: 3
Connection: close

hi
---
GET /from-curl HTTP/1.1
Host: 127.0.0.1:9000
User-Agent: curl/8.5.0
Accept: */*
X-Demo: 1

The printf supplies the response. \r\n is the line ending that HTTP requires. req.txt holds the exact request that curl sent, including the added header. This technique checks what a client sends before a real server exists.

nc as an HTTP client and a port check

Terminal
printf 'GET /hello HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n' | nc -N 127.0.0.1 8081
nc -zv 127.0.0.1 8081
nc -zv 127.0.0.1 9; echo "exit=$?"
Output
HTTP/1.1 200 OK
Server: demo/1.0 
Date: Sat, 03 Oct 2026 15:19:02 GMT
Content-Type: text/plain
Content-Length: 6

hello
Connection to 127.0.0.1 8081 port [tcp/tproxy] succeeded!
nc: connect to 127.0.0.1 port 9 (tcp) failed: Connection refused
exit=1

The first command writes an HTTP request by hand. The response shows that HTTP is plain text over TCP. -z scans without sending data and -v prints the result. Port 8081 accepted the connection. Port 9 refused it and nc exited with 1, so nc -z host port works as a check in scripts. The label tproxy is the name that the system's service list assigns to port 8081; it has no meaning for this server.

Variant Listen on port 9000 Close after input ends
OpenBSD nc (Debian, Ubuntu, macOS) nc -l 9000 -N (OpenBSD); on macOS see man nc for the equivalent
ncat (nmap) ncat -l 9000 see ncat --help
GNU or traditional nc nc -l -p 9000 -q 0

Run nc -h to see which variant is installed.

Example 12: addresses, names, and DNS

DNS (the Domain Name System) turns a name into an address. The sources are consulted in a fixed order, which on Linux is set in /etc/nsswitch.conf. Typically the order is /etc/hosts first, then the DNS servers listed in /etc/resolv.conf.

ip addr and hostname

Terminal
ip -brief addr show lo
ip -4 addr show lo
ip route
hostname
hostname -I
Output
lo               UNKNOWN        127.0.0.1/8 
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
default via 192.0.2.1 dev eth0 
192.0.2.0/24 dev eth0 proto kernel scope link src 192.0.2.2 
vm
192.0.2.2 

ip addr show lo lists the loopback interface lo and its address 127.0.0.1/8. The /8 is the prefix length: the first 8 bits identify the network. -brief prints one line per interface and -4 limits output to IPv4. Without an interface name, the command lists every interface. ip route shows where packets go: the default via line is the gateway for any address not on a listed network. hostname prints the machine name. hostname -I prints its addresses, here 192.0.2.2, a documentation address. The values depend on the machine.

Task Linux macOS
List addresses ip addr or ip -brief addr ifconfig
Show the routing table ip route netstat -rn
Addresses of this host hostname -I ipconfig getifaddr en0 (interface name varies)

/etc/hosts

/etc/hosts maps names to addresses before any DNS query happens. Each line is an address followed by one or more names.

Terminal
grep localhost /etc/hosts
getent hosts localhost
curl -s --resolve demo.test:8081:127.0.0.1 http://demo.test:8081/hello
Output
127.0.0.1 localhost
127.0.0.1       localhost
hello

grep shows the line in the file. getent hosts NAME asks the system resolver, which follows the order in /etc/nsswitch.conf, so it reports what a program would get. Editing /etc/hosts needs root and changes the answer for every program on the machine. A line such as 127.0.0.1 demo.test makes demo.test reach the local machine.

curl --resolve HOST:PORT:ADDRESS has the same effect for one command only. The request above sent Host: demo.test:8081 to 127.0.0.1, without a change to any file. This is the safe way to test a site before DNS points at it.

dig and nslookup

dig sends a DNS query and prints the answer. A real name server is not available in this environment, so the examples start a small local DNS server on UDP port 5353. It is written for this lesson and knows two names. Save it as dns.py:

Terminal
cat > dns.py <<'PY'
import socket, struct
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("127.0.0.1", 5353))
RECORDS = {"demo.test": "10.1.2.3", "shop.demo.test": "10.1.2.4"}
while True:
    data, addr = s.recvfrom(512)
    i = 12
    labels = []
    while data[i]:
        n = data[i]; labels.append(data[i+1:i+1+n].decode()); i += n + 1
    name = ".".join(labels).lower()
    question = data[12:i + 5]
    ip = RECORDS.get(name)
    if ip:
        hdr = data[:2] + struct.pack(">HHHHH", 0x8580, 1, 1, 0, 0)
        ans = b"\xc0\x0c" + struct.pack(">HHIH", 1, 1, 300, 4) + socket.inet_aton(ip)
        s.sendto(hdr + question + ans, addr)
    else:
        hdr = data[:2] + struct.pack(">HHHHH", 0x8583, 1, 0, 0, 0)
        s.sendto(hdr + question, addr)
PY
python3 dns.py &
sleep 0.5
dig @127.0.0.1 -p 5353 demo.test +noall +answer
dig @127.0.0.1 -p 5353 shop.demo.test +short
dig @127.0.0.1 -p 5353 nothere.test +noall +comments | grep status
nslookup -type=A -port=5353 demo.test 127.0.0.1
Output
demo.test.		300	IN	A	10.1.2.3
10.1.2.4
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 43656
Server:		127.0.0.1
Address:	127.0.0.1#5353

Name:	demo.test
Address: 10.1.2.3

The id in the header line varies between runs. In the first answer, the columns are the name, the time to live in seconds (300), the class (IN), the record type (A, an IPv4 address), and the address. +noall +answer prints only the answer section. +short prints only the value. The third query asks for a name the server does not know; the header shows status: NXDOMAIN, the code for "no such name". nslookup asks the same question in a different layout. nslookup NAME SERVER queries a chosen server, and -type=A limits the question to IPv4 addresses.

Option Effect
@SERVER Ask this DNS server instead of the one in /etc/resolv.conf
-p PORT Use a port other than 53
TYPE after the name (A, AAAA, MX, TXT, NS, CNAME) Choose the record type
+short Print only the values
+trace Follow the delegation from the root servers
-x ADDRESS Reverse lookup, from address to name

Stop the DNS server afterwards with kill %1 in the same shell, or find it with ss -ulnp 'sport = :5353'.

Queries against public names need the network, so the next output is representative. It shows the form of the answer, with a documentation address in place of the real one.

Terminal
dig +short example.com
Output
203.0.113.10

Representative output: the real address of a public name changes between locations and over time.

ping

ping sends ICMP echo requests, small packets that a reachable host answers, and prints the round-trip time of each. ping is not installed on the machine used to produce this lesson, so the output below is representative of GNU iputils ping against a public host.

Terminal
ping -c 3 example.com
Output
PING example.com (203.0.113.10) 56(84) bytes of data.
64 bytes from 203.0.113.10: icmp_seq=1 ttl=57 time=11.8 ms
64 bytes from 203.0.113.10: icmp_seq=2 ttl=57 time=11.4 ms
64 bytes from 203.0.113.10: icmp_seq=3 ttl=57 time=11.6 ms

--- example.com ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 11.402/11.600/11.801/0.162 ms

Representative output. -c 3 stops after three packets; without it, ping runs until interrupted on both Linux and macOS. ttl is the remaining hop limit, and time is the round-trip time. A host that does not answer does not necessarily fail: firewalls often drop ICMP while allowing TCP. A successful curl or nc -z to the real service port is the stronger check.

Finally, stop the background servers and confirm that the ports are free:

Terminal
kill %1 %2 %3
ss -tln | grep -E ':(8081|8082|9000)\b' || echo "servers stopped"
Output
servers stopped

kill %N stops background job number N of the current shell. When the servers were started from another shell, find the process ids with ss -tlnp or lsof -i :8081 and pass them to kill.

Interview points

  • curl exits with 0 for any completed transfer, including a 500 response. -f turns status 400 and above into exit code 22, and exit code 7 means the connection failed.
  • -d implies POST and the form content type. --json sets Content-Type and Accept to JSON. -X only changes the method name and does not change the body or headers.
  • -I sends HEAD; -i keeps GET and prints headers. -v shows the request lines as well as the response headers.
  • -L is required to follow redirects, and curl removes credentials when a redirect changes host unless --location-trusted is set.
  • ss -tlnp lists listening TCP sockets with owning processes. A local address of 127.0.0.1 means other machines cannot connect, while 0.0.0.0 or [::] accepts connections on every interface.
  • lsof -i :PORT and ss -tlnp 'sport = :PORT' answer which process holds a port. Seeing other users' processes needs root.
  • nc -z HOST PORT tests whether a TCP port accepts connections without sending data. nc flags differ between the OpenBSD, ncat, and traditional variants.
  • Name lookup order comes from /etc/nsswitch.conf, usually /etc/hosts first and then DNS. dig queries DNS directly and skips /etc/hosts, while getent hosts follows the system order.
  • curl --resolve HOST:PORT:ADDRESS tests a name against a chosen address without editing /etc/hosts or DNS.
  • A failed ping does not prove a host is down, because ICMP is often blocked. A TCP check on the service port is more reliable.

Summary

  • curl URL sends a GET and prints the body; -s hides the progress meter and -S restores error messages.
  • -v prints connection steps (*), the request (>), and the response headers (<) on standard error.
  • -i includes response headers; -I sends HEAD and prints headers only.
  • -X sets the method, -H adds a header, -d sends a body and implies POST, and --json sends JSON with matching headers.
  • -o FILE and -O save the body; -L follows redirects; -C - resumes a download.
  • -w '%{http_code}' prints transfer facts, -f turns HTTP errors into exit code 22, and --retry N repeats transient failures.
  • ss -tlnp is the Linux tool for listening sockets, netstat -tlnp is the older equivalent, and lsof -i works on Linux and macOS.
  • A socket bound to 127.0.0.1 accepts local connections only.
  • nc -l PORT listens and nc HOST PORT connects; nc -z tests a port.
  • ip addr lists interface addresses, ip route lists routes, and hostname -I prints the machine's addresses.
  • /etc/hosts is consulted before DNS; dig and nslookup query DNS servers; ping sends ICMP echo requests.
  • Output that depends on the public internet, such as dig on a public name and ping, varies by location and time.