diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..f336a9fbd --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## Goal + + +## Changes +- + +## Testing + + +## Checklist +- [ ] Title is a clear sentence (≤ 70 chars) +- [ ] Commits are signed (`git log --show-signature`) +- [ ] `submissions/labN.md` updated diff --git a/.gitignore b/.gitignore index 1c0a1e94b..966b15200 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,5 @@ Thumbs.db # *.sbom.cdx.json, zap-*.html/json, trivy-*.txt (Lab 9 scan evidence) # flake.nix, flake.lock (Lab 11) # wasm/main.go, spin.toml, go.sum (Lab 12) +data/ +app/data/ diff --git a/evidence/lab4/01-pcap-read.txt b/evidence/lab4/01-pcap-read.txt new file mode 100644 index 000000000..58a16cf5c --- /dev/null +++ b/evidence/lab4/01-pcap-read.txt @@ -0,0 +1,45 @@ +18:50:14.556151 IP 127.0.0.1.52742 > 127.0.0.1.8080: Flags [S], seq 852912346, win 65495, options [mss 65495,sackOK,TS val 2230267369 ecr 0,nop,wscale 10], length 0 +E..<..@.@.;.............2.h..........0......... +..-........ + +18:50:14.556164 IP 127.0.0.1.8080 > 127.0.0.1.52742: Flags [S.], seq 3354113025, ack 852912347, win 65483, options [mss 65495,sackOK,TS val 407459794 ecr 2230267369,nop,wscale 10], length 0 +E..<..@.@.<.................2.h......0......... +.IW...-.... + +18:50:14.556173 IP 127.0.0.1.52742 > 127.0.0.1.8080: Flags [.], ack 1, win 64, options [nop,nop,TS val 2230267369 ecr 407459794], length 0 +E..4..@.@.;.............2.h........@.(..... +..-..IW. +18:50:14.556203 IP 127.0.0.1.52742 > 127.0.0.1.8080: Flags [P.], seq 1:176, ack 1, win 64, options [nop,nop,TS val 2230267369 ecr 407459794], length 175: HTTP: POST /notes HTTP/1.1 +E.....@.@.;6............2.h........@....... +..-..IW.POST /notes HTTP/1.1 +Host: localhost:8080 +User-Agent: curl/8.5.0 +Accept: */* +Content-Type: application/json +Content-Length: 40 + +{"title":"lab4","body":"packet capture"} +18:50:14.556207 IP 127.0.0.1.8080 > 127.0.0.1.52742: Flags [.], ack 176, win 64, options [nop,nop,TS val 407459794 ecr 2230267369], length 0 +E..4.q@.@..P................2.i....@.(..... +.IW...-. +18:50:14.556662 IP 127.0.0.1.8080 > 127.0.0.1.52742: Flags [P.], seq 1:208, ack 176, win 64, options [nop,nop,TS val 407459794 ecr 2230267369], length 207: HTTP: HTTP/1.1 201 Created +E....r@.@...................2.i....@....... +.IW...-.HTTP/1.1 201 Created +Content-Type: application/json +Date: Thu, 06 Aug 2026 15:50:14 GMT +Content-Length: 94 + +{"id":6,"title":"lab4","body":"packet capture","created_at":"2026-08-06T15:50:14.556405793Z"} + +18:50:14.556696 IP 127.0.0.1.52742 > 127.0.0.1.8080: Flags [.], ack 208, win 64, options [nop,nop,TS val 2230267369 ecr 407459794], length 0 +E..4..@.@.;.............2.i........@.(..... +..-..IW. +18:50:14.556827 IP 127.0.0.1.52742 > 127.0.0.1.8080: Flags [F.], seq 176, ack 208, win 64, options [nop,nop,TS val 2230267369 ecr 407459794], length 0 +E..4..@.@.;.............2.i........@.(..... +..-..IW. +18:50:14.556994 IP 127.0.0.1.8080 > 127.0.0.1.52742: Flags [F.], seq 208, ack 177, win 64, options [nop,nop,TS val 407459794 ecr 2230267369], length 0 +E..4.s@.@..N................2.i....@.(..... +.IW...-. +18:50:14.557066 IP 127.0.0.1.52742 > 127.0.0.1.8080: Flags [.], ack 209, win 64, options [nop,nop,TS val 2230267370 ecr 407459794], length 0 +E..4..@.@.;.............2.i........@.(..... +..-..IW. diff --git a/evidence/lab4/02-diagnostics.txt b/evidence/lab4/02-diagnostics.txt new file mode 100644 index 000000000..bb8530cd3 --- /dev/null +++ b/evidence/lab4/02-diagnostics.txt @@ -0,0 +1,93 @@ +$ ss -tlnp | head -20 +State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess +LISTEN 0 10 0.0.0.0:7070 0.0.0.0:* +LISTEN 0 50 0.0.0.0:139 0.0.0.0:* +LISTEN 0 4096 127.0.0.1:10808 0.0.0.0:* +LISTEN 0 50 0.0.0.0:445 0.0.0.0:* +LISTEN 0 5 0.0.0.0:902 0.0.0.0:* +LISTEN 0 32 192.168.100.1:53 0.0.0.0:* +LISTEN 0 4096 127.0.0.1:11434 0.0.0.0:* +LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* +LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* +LISTEN 0 4096 127.0.0.1:631 0.0.0.0:* +LISTEN 0 4096 127.0.0.1:33357 0.0.0.0:* +LISTEN 0 32 192.168.122.1:53 0.0.0.0:* +LISTEN 0 10 [::]:7070 [::]:* +LISTEN 0 4096 [::1]:631 [::]:* +LISTEN 0 4096 *:8080 *:* users:(("quicknotes",pid=5201,fd=3)) +LISTEN 0 50 [::]:139 [::]:* +LISTEN 0 50 [::]:445 [::]:* +LISTEN 0 5 [::]:902 [::]:* + +$ ss -tn state established +Recv-Q Send-Q Local Address:Port Peer Address:Port Process +0 0 127.0.0.1:10808 127.0.0.1:49374 +0 0 192.168.0.123%enp5s0:40090 192.227.253.103:50000 +0 0 127.0.0.1:39186 127.0.0.1:10808 +0 0 192.168.0.123%enp5s0:45180 192.227.253.103:50000 +0 0 192.168.0.123%enp5s0:52388 192.227.253.103:50000 +0 0 192.168.0.123%enp5s0:52400 192.227.253.103:50000 +0 0 127.0.0.1:49374 127.0.0.1:10808 +0 0 127.0.0.1:10808 127.0.0.1:39186 +0 0 10.33.0.2:53448 207.188.6.17:443 + +$ ip -brief addr +lo UNKNOWN 127.0.0.1/8 +enp5s0 UP 192.168.0.123/24 +wlx9ca2f49134b7 UP 192.168.0.120/24 +virbr0 DOWN 192.168.122.1/24 +virbr1 DOWN 192.168.100.1/24 +docker0 DOWN 172.17.0.1/16 +tun2 UNKNOWN 10.33.0.2/24 + +$ ip route +default via 192.168.0.1 dev enp5s0 proto dhcp src 192.168.0.123 metric 100 +default via 192.168.0.1 dev wlx9ca2f49134b7 proto dhcp src 192.168.0.120 metric 600 +1.0.0.0/8 via 10.33.0.2 dev tun2 +1.0.0.1 via 10.33.0.2 dev tun2 +2.0.0.0/7 via 10.33.0.2 dev tun2 +4.0.0.0/6 via 10.33.0.2 dev tun2 +8.0.0.0/5 via 10.33.0.2 dev tun2 +10.33.0.0/24 dev tun2 proto kernel scope link src 10.33.0.2 +16.0.0.0/4 via 10.33.0.2 dev tun2 +32.0.0.0/3 via 10.33.0.2 dev tun2 +64.0.0.0/2 via 10.33.0.2 dev tun2 +127.0.0.0/8 dev lo proto kernel scope link src 127.0.0.1 metric 30 +128.0.0.0/1 via 10.33.0.2 dev tun2 +172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown +172.29.172.254 via 10.33.0.2 dev tun2 +192.168.0.0/24 dev enp5s0 proto kernel scope link src 192.168.0.123 metric 100 +192.168.0.0/24 dev wlx9ca2f49134b7 proto kernel scope link src 192.168.0.120 metric 600 +192.168.100.0/24 dev virbr1 proto kernel scope link src 192.168.100.1 linkdown +192.168.122.0/24 dev virbr0 proto kernel scope link src 192.168.122.1 linkdown + +$ dig +short localhost +127.0.0.1 + +$ dig localhost | head -20 + +; <<>> DiG 9.18.39-0ubuntu0.24.04.5-Ubuntu <<>> localhost +;; global options: +cmd +;; Got answer: +;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18509 +;; flags: qr aa rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 + +;; OPT PSEUDOSECTION: +; EDNS: version: 0, flags:; udp: 65494 +;; QUESTION SECTION: +;localhost. IN A + +;; ANSWER SECTION: +localhost. 0 IN A 127.0.0.1 + +;; Query time: 0 msec +;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP) +;; WHEN: Thu Aug 06 18:52:47 MSK 2026 +;; MSG SIZE rcvd: 54 + + +$ getent hosts localhost +127.0.0.1 localhost + +$ curl -s -o /dev/null -w "dns=%{time_namelookup} connect=%{time_connect} total=%{time_total}\n" http://localhost:8080/health +dns=0.000031 connect=0.000240 total=0.000772 diff --git a/evidence/lab4/03-path.txt b/evidence/lab4/03-path.txt new file mode 100644 index 000000000..534e06beb --- /dev/null +++ b/evidence/lab4/03-path.txt @@ -0,0 +1,26 @@ +$ mtr -rwc 5 localhost +Start: 2026-08-06T18:53:36+0300 +HOST: hns Loss% Snt Last Avg Best Wrst StDev + 1.|-- localhost 0.0% 5 0.1 0.1 0.1 0.1 0.0 + +$ ip link show lo +1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 + link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 + +$ cat /proc/sys/net/ipv4/tcp_congestion_control +cubic + +$ ss -tim state established dst 127.0.0.1 | head -20 +Recv-Q Send-Q Local Address:Port Peer Address:Port Process +0 0 127.0.0.1:10808 127.0.0.1:49374 + skmem:(r0,rb170241,t0,tb2626560,f0,w0,o0,bl0,d6) cubic wscale:10,10 rto:201 rtt:0.601/0.329 ato:40 mss:65483 pmtu:65535 rcvmss:20480 advmss:65483 cwnd:27 bytes_sent:1645335 bytes_acked:1645335 bytes_received:1146513 segs_out:585 segs_in:386 data_segs_out:493 data_segs_in:112 send 23.5Gbps lastsnd:31928 lastrcv:34024 lastack:31928 pacing_rate 47Gbps delivery_rate 74.8Gbps delivered:494 app_limited busy:988ms rcv_rtt:640.625 rcv_space:70448 rcv_ssthresh:161157 minrtt:0.007 snd_wnd:325632 +0 0 127.0.0.1:10808 127.0.0.1:35028 + skmem:(r0,rb131072,t0,tb2626560,f0,w0,o0,bl0,d1) cubic wscale:10,10 rto:206 rtt:5.112/10.083 ato:40 mss:32768 pmtu:65535 rcvmss:536 advmss:65483 cwnd:10 bytes_sent:12 bytes_acked:12 bytes_received:13 segs_out:4 segs_in:7 data_segs_out:2 data_segs_in:2 send 513Mbps lastsnd:32576 lastrcv:32576 lastack:32535 pacing_rate 1.03Gbps delivery_rate 3.45Gbps delivered:3 app_limited busy:41ms rcv_space:65483 rcv_ssthresh:65483 minrtt:0.076 snd_wnd:65536 +0 0 127.0.0.1:35028 127.0.0.1:10808 + skmem:(r0,rb131072,t0,tb2626560,f0,w0,o0,bl0,d0) cubic wscale:10,10 rto:201 rtt:0.133/0.047 ato:40 mss:32768 pmtu:65535 rcvmss:536 advmss:65483 cwnd:10 bytes_sent:13 bytes_acked:14 bytes_received:12 segs_out:7 segs_in:5 data_segs_out:2 data_segs_in:2 send 19.7Gbps lastsnd:32578 lastrcv:32578 lastack:2113 pacing_rate 39.2Gbps delivery_rate 2.57Gbps delivered:3 app_limited rcv_space:65495 rcv_ssthresh:65495 minrtt:0.102 snd_wnd:65536 +0 0 127.0.0.1:39186 127.0.0.1:10808 + skmem:(r0,rb131072,t0,tb2626560,f0,w0,o0,bl0,d0) cubic wscale:10,10 rto:203 rtt:2.618/5.187 ato:40 mss:34304 pmtu:65535 rcvmss:1391 advmss:65483 cwnd:10 bytes_sent:2311 bytes_acked:2312 bytes_received:1680 segs_out:29 segs_in:32 data_segs_out:8 data_segs_in:7 send 1.05Gbps lastsnd:520092 lastrcv:519745 lastack:6210 pacing_rate 2.1Gbps delivery_rate 19.6Gbps delivered:9 app_limited busy:42ms rcv_space:65495 rcv_ssthresh:76663 minrtt:0.014 snd_wnd:68608 +0 0 127.0.0.1:49374 127.0.0.1:10808 + skmem:(r0,rb410514,t0,tb2626560,f0,w0,o0,bl0,d0) cubic wscale:10,10 rto:201 rtt:0.256/0.421 ato:40 mss:65483 pmtu:65535 rcvmss:8568 advmss:65483 cwnd:10 bytes_sent:1146513 bytes_acked:1146514 bytes_received:1645335 segs_out:386 segs_in:586 data_segs_out:112 data_segs_in:493 send 20.5Gbps lastsnd:34029 lastrcv:31933 lastack:1603 pacing_rate 40.9Gbps delivery_rate 105Gbps delivered:113 app_limited busy:210ms rcv_rtt:16 rcv_space:134229 rcv_ssthresh:351181 minrtt:0.005 snd_wnd:161792 +0 0 127.0.0.1:10808 127.0.0.1:39186 + skmem:(r0,rb131072,t0,tb2626560,f0,w0,o0,bl0,d17) cubic wscale:10,10 rto:202 rtt:1.9/2.611 ato:40 mss:38400 pmtu:65535 rcvmss:1085 advmss:65483 cwnd:10 bytes_sent:1680 bytes_acked:1680 bytes_received:2311 segs_out:31 segs_in:29 data_segs_out:7 data_segs_in:8 send 1.62Gbps lastsnd:519749 lastrcv:520096 lastack:519749 pacing_rate 3.23Gbps delivery_rate 16.2Gbps delivered:8 app_limited busy:18ms rcv_rtt:4 rcv_space:65483 rcv_ssthresh:68534 minrtt:0.019 snd_wnd:76800 diff --git a/evidence/lab4/04-refused.txt b/evidence/lab4/04-refused.txt new file mode 100644 index 000000000..a2675e337 --- /dev/null +++ b/evidence/lab4/04-refused.txt @@ -0,0 +1,13 @@ +$ curl -sv http://localhost:9999/health +* Host localhost:9999 was resolved. +* IPv6: ::1 +* IPv4: 127.0.0.1 +* Trying [::1]:9999... +* Immediate connect fail for ::1: Невозможно назначить запрошенный адрес +* Trying 127.0.0.1:9999... +* connect to 127.0.0.1 port 9999 from 127.0.0.1 port 58714 failed: В соединении отказано +* Failed to connect to localhost port 9999 after 0 ms: Couldn't connect to server +* Closing connection + +$ ss -tln sport = :9999 +State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess diff --git a/evidence/lab4/05-latency.txt b/evidence/lab4/05-latency.txt new file mode 100644 index 000000000..b0f190811 --- /dev/null +++ b/evidence/lab4/05-latency.txt @@ -0,0 +1,33 @@ +$ 20 последовательных запросов, тайминги +0.000349 0.000828 0.000901 +0.000365 0.000775 0.000841 +0.000207 0.000502 0.000542 +0.000255 0.000540 0.000604 +0.000139 0.000299 0.000324 +0.000157 0.000355 0.000385 +0.000129 0.000285 0.000309 +0.000124 0.000271 0.000295 +0.000119 0.000260 0.000282 +0.000128 0.000273 0.000298 +0.000107 0.000231 0.000250 +0.000119 0.000283 0.000304 +0.000110 0.000246 0.000266 +0.000128 0.000283 0.000307 +0.000098 0.000203 0.000221 +0.000110 0.000232 0.000252 +0.000107 0.000231 0.000254 +0.000111 0.000279 0.000299 +0.000091 0.000213 0.000233 +0.000086 0.000181 0.000196 + +$ ss -s +Total: 972 +TCP: 49 (estab 10, closed 21, orphaned 0, timewait 21) + +Transport Total IP IPv6 +RAW 3 1 2 +UDP 39 35 4 +TCP 28 22 6 +INET 70 58 12 +FRAG 0 0 0 + diff --git a/evidence/lab4/06-broken-deploy.txt b/evidence/lab4/06-broken-deploy.txt new file mode 100644 index 000000000..7d1c122a0 --- /dev/null +++ b/evidence/lab4/06-broken-deploy.txt @@ -0,0 +1,3 @@ +2026/08/06 18:55:51 quicknotes listening on :8080 (notes loaded: 6) +2026/08/06 18:55:51 listen: listen tcp :8080: bind: address already in use +exit status 1 diff --git a/evidence/lab4/07-triage.txt b/evidence/lab4/07-triage.txt new file mode 100644 index 000000000..4db993e7a --- /dev/null +++ b/evidence/lab4/07-triage.txt @@ -0,0 +1,19 @@ +=== 1. Симптом: клиент не может подключиться к новому инстансу === +$ curl -sv --max-time 3 http://localhost:8080/health +{"notes":6,"status":"ok"} + + +=== 2. Кто занимает порт 8080 === +$ ss -tlnp sport = :8080 +State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess +LISTEN 0 4096 *:8080 *:* users:(("quicknotes",pid=5201,fd=3)) + +=== 3. Что это за процесс === +$ ps -p 5201 -o pid,ppid,etime,cmd + PID PPID ELAPSED CMD + 5201 5094 09:29 /tmp/go-build2908606575/b001/exe/quicknotes + +=== 4. Когда он стартовал === +$ ls -la /proc/5201/cwd +lrwxrwxrwx 1 hns hns 0 авг 6 18:56 /proc/5201/cwd -> /home/hns/dev/DevOps-Intro/app +lrwxrwxrwx 1 hns hns 0 авг 6 18:56 /proc/5201/cwd -> /home/hns/dev/DevOps-Intro/app diff --git a/evidence/lab4/08-fix.txt b/evidence/lab4/08-fix.txt new file mode 100644 index 000000000..e9320c80e --- /dev/null +++ b/evidence/lab4/08-fix.txt @@ -0,0 +1,14 @@ +=== Fix: остановить старый инстанс === +$ kill 5201 +$ ss -tlnp sport = :8080 (порт должен освободиться) +State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess +(пусто = порт свободен) + +=== Recovery: новый инстанс поднят === +$ ss -tlnp sport = :8080 +State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess +LISTEN 0 4096 *:8080 *:* users:(("quicknotes",pid=5820,fd=3)) + +$ curl -s http://localhost:8080/health +{"notes":6,"status":"ok"} + diff --git a/evidence/lab4/quicknotes.pcap b/evidence/lab4/quicknotes.pcap new file mode 100644 index 000000000..2cee86ac8 Binary files /dev/null and b/evidence/lab4/quicknotes.pcap differ diff --git a/submissions/lab4.md b/submissions/lab4.md new file mode 100644 index 000000000..9ec0f92df --- /dev/null +++ b/submissions/lab4.md @@ -0,0 +1,354 @@ +# Lab 4 — OS & Networking: Trace a Request, Debug a Deploy + +**Author:** HNS ([@HNS2112](https://github.com/HNS2112)) +**Date:** 6 August 2026 +**Environment:** Ubuntu 24.04 (noble), Linux kernel with systemd-resolved, Go 1.26.5 + +Raw command output is committed under `evidence/lab4/`. + +--- + +## Task 1 — Trace One Request End to End + +### 1.1 Packet capture of a single POST + +`tcpdump -i lo -w quicknotes.pcap 'tcp port 8080'` running while a single +`POST /notes` was issued. The capture contains the entire life of one TCP +connection in ten packets: + +```console +18:50:14.556151 IP 127.0.0.1.52742 > 127.0.0.1.8080: Flags [S], seq 852912346, win 65495 +18:50:14.556164 IP 127.0.0.1.8080 > 127.0.0.1.52742: Flags [S.], seq 3354113025, ack 852912347 +18:50:14.556173 IP 127.0.0.1.52742 > 127.0.0.1.8080: Flags [.], ack 1 +18:50:14.556203 IP 127.0.0.1.52742 > 127.0.0.1.8080: Flags [P.], length 175: HTTP: POST /notes HTTP/1.1 +POST /notes HTTP/1.1 +Host: localhost:8080 +User-Agent: curl/8.5.0 +Content-Type: application/json +Content-Length: 40 +{"title":"lab4","body":"packet capture"} +18:50:14.556207 IP 127.0.0.1.8080 > 127.0.0.1.52742: Flags [.], ack 176 +18:50:14.556662 IP 127.0.0.1.8080 > 127.0.0.1.52742: Flags [P.], length 207: HTTP: HTTP/1.1 201 Created +HTTP/1.1 201 Created +Content-Type: application/json +Content-Length: 94 +{"id":6,"title":"lab4","body":"packet capture","created_at":"2026-08-06T15:50:14.556405793Z"} +18:50:14.556696 IP 127.0.0.1.52742 > 127.0.0.1.8080: Flags [.], ack 208 +18:50:14.556827 IP 127.0.0.1.52742 > 127.0.0.1.8080: Flags [F.], seq 176 +18:50:14.556994 IP 127.0.0.1.8080 > 127.0.0.1.52742: Flags [F.], seq 208 +18:50:14.557066 IP 127.0.0.1.52742 > 127.0.0.1.8080: Flags [.], ack 209 +``` + +Mapping the four required phases onto the packets: + +| Phase | Packets | Flags | +|---|---|---| +| TCP three-way handshake | 1–3 | `[S]`, `[S.]`, `[.]` | +| HTTP request | 4 | `[P.]`, 175 bytes | +| HTTP response | 6 | `[P.]`, 207 bytes, `201 Created` | +| Connection teardown | 8–10 | `[F.]`, `[F.]`, `[.]` | + +Timings worth noting: the handshake completed in 22 microseconds +(`.556151` → `.556173`) and the whole exchange in 915 microseconds. Nothing here +leaves the kernel — see §1.3 on why loopback numbers look like this. + +The request and response each fit in a single segment. That is not luck; §1.3 +explains why nothing needed fragmenting. + +### 1.2 Diagnostic commands + +```console +$ ss -tlnp sport = :8080 +State Recv-Q Send-Q Local Address:Port Peer Address:Port Process +LISTEN 0 4096 *:8080 *:* users:(("quicknotes",pid=5201,fd=3)) + +$ ip -brief addr +lo UNKNOWN 127.0.0.1/8 +enp5s0 UP 192.168.0.123/24 +wlx9ca2f49134b7 UP 192.168.0.120/24 +virbr0 DOWN 192.168.122.1/24 +virbr1 DOWN 192.168.100.1/24 +docker0 DOWN 172.17.0.1/16 +tun2 UNKNOWN 10.33.0.2/24 + +$ ip route | head -3 +default via 192.168.0.1 dev enp5s0 proto dhcp src 192.168.0.123 metric 100 +default via 192.168.0.1 dev wlx9ca2f49134b7 proto dhcp src 192.168.0.120 metric 600 +127.0.0.0/8 dev lo proto kernel scope link src 127.0.0.1 metric 30 + +$ curl -s -o /dev/null -w "dns=%{time_namelookup} connect=%{time_connect} total=%{time_total}\n" \ + http://localhost:8080/health +dns=0.000031 connect=0.000240 total=0.000772 +``` + +**The listener is bound to `*:8080`, not to loopback.** This machine has seven +interfaces including two with routable LAN addresses, so QuickNotes — which has +no authentication — is reachable from the local network, not just from this host. +The application reads its bind address from `ADDR`, so `ADDR=127.0.0.1:8080` +would restrict it. This is the same environment variable used to break the deploy +in Task 2, and worth remembering for the container work in Lab 6. + +### 1.3 Path and transport properties + +```console +$ mtr -rwc 5 localhost +HOST: hns Loss% Snt Last Avg Best Wrst StDev + 1.|-- localhost 0.0% 5 0.1 0.1 0.1 0.1 0.0 + +$ ip link show lo +1: lo: mtu 65536 qdisc noqueue state UNKNOWN + +$ cat /proc/sys/net/ipv4/tcp_congestion_control +cubic + +$ ss -tim state established dst 127.0.0.1 +... cubic wscale:10,10 rto:201 rtt:0.256/0.421 mss:65483 pmtu:65535 cwnd:10 + send 20.5Gbps pacing_rate 40.9Gbps delivery_rate 105Gbps minrtt:0.005 +``` + +Three things explain the microsecond timings in §1.1. + +**MTU on `lo` is 65536**, against the 1500 typical of Ethernet. The negotiated +MSS is 65483 bytes, so the 175-byte request and 207-byte response fit in one +segment each with room to spare. There is nothing to fragment and no +path-MTU discovery to do. + +**`qdisc noqueue`** — loopback has no queueing discipline at all. There is no +buffer to fill and therefore no queueing delay. + +**`delivery_rate 105Gbps`** is not a network speed. It is the rate at which the +kernel copies between socket buffers in RAM; the packet never reaches a NIC +driver. `minrtt:0.005` — five microseconds — is the same fact from the latency +side. + +The congestion control algorithm is `cubic`, and `cwnd:10` shows the connection +never left the initial window. On a link with no loss and no queueing, congestion +control has nothing to react to. + +### 1.4 Name resolution — and a correction + +```console +$ dig +short localhost +127.0.0.1 + +$ dig localhost | head -20 +;; flags: qr aa rd ra ad; QUERY: 1, ANSWER: 1 +localhost. 0 IN A 127.0.0.1 +;; Query time: 0 msec +;; SERVER: 127.0.0.53#53(127.0.0.53) (UDP) + +$ getent hosts localhost +127.0.0.1 localhost +``` + +The expectation going in was that `dig localhost` would return nothing, on the +grounds that `localhost` is resolved from `/etc/hosts` rather than by DNS. That +turned out to be wrong on this system, and the reason is visible in the output: +`SERVER: 127.0.0.53#53` is `systemd-resolved`, which intercepts DNS on a loopback +stub address and synthesises an answer for `localhost` itself. The `aa` flag +marks it authoritative and `Query time: 0 msec` shows nothing left the machine. + +So both paths work, but through different mechanisms: `getent` consults NSS +(`/etc/hosts` first, per `/etc/nsswitch.conf`), while `dig` speaks DNS to a local +stub resolver that answers from its own knowledge. `curl` uses the NSS path, +which is why `time_namelookup` was 31 microseconds. + +The practical lesson is the one behind "it's never DNS": `dig` and the +application may not be asking the same question of the same resolver. Reproducing +a name-resolution problem with `dig` alone can mislead. + +### 1.5 Connection refused vs. timeout + +```console +$ curl -sv --max-time 3 http://localhost:9999/health +* Host localhost:9999 was resolved. +* IPv6: ::1 +* IPv4: 127.0.0.1 +* Trying [::1]:9999... +* Immediate connect fail for ::1: Cannot assign requested address +* Trying 127.0.0.1:9999... +* connect to 127.0.0.1 port 9999 from 127.0.0.1 port 58714 failed: Connection refused +* Failed to connect to localhost port 9999 after 0 ms + +$ ss -tln sport = :9999 +State Recv-Q Send-Q Local Address:Port Peer Address:Port + (empty) +``` + +The failure is immediate — "after 0 ms" — because nothing is on the wire. The +kernel knows no socket is listening on 9999 and answers the SYN with a RST +itself. `ss` confirms the port is unbound. + +This is the diagnostic difference that matters in an incident. **Connection +refused means something answered and said no**: the host is up, the route works, +the process is down. **A timeout means nothing answered at all**: a firewall +dropping packets, a wrong route, a dead host, or a process too wedged to accept. +The two point at completely different parts of the stack, and confusing them +sends the investigation the wrong way. + +The IPv6 attempt is a secondary detail worth noting: curl tried `::1` first and +failed differently — "cannot assign requested address" rather than "refused" — +because IPv6 is not configured on loopback here. Happy Eyeballs then fell back to +IPv4 with no measurable delay. + +### 1.6 Latency under repeated requests + +Twenty sequential `GET /health` calls, `connect` / `starttransfer` / `total` +in seconds: + +``` +0.000349 0.000828 0.000901 <- first +0.000365 0.000775 0.000841 +0.000207 0.000502 0.000542 +... +0.000091 0.000213 0.000233 +0.000086 0.000181 0.000196 <- twentieth +``` + +The first request took 901 µs and the twentieth 196 µs — a 4.6× speedup with no +change to the code or the request. The warm-up is in the CPU caches, the +scheduler's picture of these two processes, and the Go runtime's own state. + +This is why a single measurement of anything is untrustworthy, and it is the same +effect that produced the 18-second spread in the CI timings in Lab 3. + +```console +$ ss -s +TCP: 49 (estab 10, closed 21, orphaned 0, timewait 21) +``` + +The 21 sockets in TIME_WAIT are the closed connections from this loop. TIME_WAIT +is the 2×MSL wait that lets late duplicate segments drain before the port pair +can be reused — the cost of the clean four-way teardown seen in §1.1. + +--- + +## Task 2 — Debug a Broken Deploy + +### 2.1 The break + +A second instance was started while the first was still running: + +```console +$ ADDR=:8080 go run . +2026/08/06 18:55:51 quicknotes listening on :8080 (notes loaded: 6) +2026/08/06 18:55:51 listen: listen tcp :8080: bind: address already in use +exit status 1 +``` + +Note the ordering in the log: the application announced it was listening +*before* the bind failed. The message is emitted from the setup path, not from a +successful `Listen` call — so a log-scraping alert keyed on "listening on" would +have recorded a successful start for a process that died one line later. + +### 2.2 Outside-in triage + +**Step 1 — reproduce the symptom from the client's side.** + +```console +$ curl -s http://localhost:8080/health +{"notes":6,"status":"ok"} +``` + +The service answers. This is the most important step in the whole exercise: from +outside, nothing is wrong. The old instance is serving happily. The failure is +not "the service is down" but "the deploy did not take" — a distinction invisible +to a health check. + +**Step 2 — who owns the port.** + +```console +$ ss -tlnp sport = :8080 +LISTEN 0 4096 *:8080 *:* users:(("quicknotes",pid=5201,fd=3)) +``` + +**Step 3 — identify the process.** + +```console +$ ps -p 5201 -o pid,ppid,etime,cmd + PID PPID ELAPSED CMD + 5201 5094 09:29 /tmp/go-build2908606575/b001/exe/quicknotes +``` + +`ELAPSED 09:29` settles it. The process holding the port has been running for +nine and a half minutes, so it is the *old* instance, not the one just launched. +`PPID 5094` is the `go run` that spawned it. + +**Step 4 — where it came from.** + +```console +$ ls -la /proc/5201/cwd +lrwxrwxrwx 1 hns hns 0 /proc/5201/cwd -> /home/hns/dev/DevOps-Intro/app +``` + +`/proc//cwd` gives the working directory of a running process, which matters +here because QuickNotes resolves `DATA_PATH` and `SEED_PATH` relative to it. + +### 2.3 The fix + +```console +$ kill 5201 +$ ss -tlnp sport = :8080 + (empty — port released) +``` + +The old instance logged `shutting down` rather than dying abruptly: it caught +SIGTERM and shut down gracefully. Restart: + +```console +$ ss -tlnp sport = :8080 +LISTEN 0 4096 *:8080 *:* users:(("quicknotes",pid=5820,fd=3)) + +$ curl -s http://localhost:8080/health +{"notes":6,"status":"ok"} +``` + +New PID, six notes intact — state survived because it lives in `data/notes.json` +on disk, not in the process. + +### 2.4 Postmortem + +**Summary.** A deploy of QuickNotes failed at 18:55:51 with +`bind: address already in use`. The previous instance kept serving traffic +throughout, so no user-visible outage occurred, but the new build was not running +for the six minutes until the conflict was noticed. Resolved at 18:57:51 by +stopping the old process and starting the new one; recovery took 53 seconds. + +**Timeline.** + +| Time | Event | +|---|---| +| 18:46:43 | instance A starts, 4 notes loaded | +| 18:55:51 | deploy attempted; instance B exits with bind error | +| 18:56:58 | instance A stopped (SIGTERM, graceful) | +| 18:57:51 | instance B starts, 6 notes loaded | + +**What went wrong.** The deploy procedure starts the new process before stopping +the old one. Both bind the same port on the same host, and the second bind +necessarily fails. No data was lost and no request was dropped. + +**Why it was hard to see.** Two things hid the failure. The health endpoint kept +returning 200, because the process answering it was alive — it was just the wrong +version. And the application logs `listening on :8080` before the bind succeeds, +so the last line before the crash reads like a successful start. + +**What would prevent it, without blaming anyone.** The procedure, not the +operator, is at fault: any sequence that overlaps two processes on one port will +fail this way every time. Three changes address it at different levels. Stopping +the old instance before starting the new one removes the conflict outright — this +is what a systemd unit does by default and what `docker compose up` does when it +recreates a container. Binding the new instance to a different port and switching +traffic only after it reports healthy avoids the downtime the simple fix +introduces. And a deploy check that compares the running build to the intended +one — a version string on `/health`, or the PID's start time — would have +surfaced the problem in seconds instead of six minutes. + +--- + +## Summary + +| Task | Status | +|------|--------| +| Task 1 — packet capture, diagnostics, DNS, refused vs. timeout | Complete | +| Task 2 — broken deploy, outside-in triage, postmortem | Complete | +| Bonus — TLS proxy and handshake capture | Not attempted |