localhost আর 127.0.0.1
Why your tests pass locally but fail in CI — and what ‘localhost’ has to do with it.
সত্য ঘটনা অবলম্বনে ।
এই article টায় এসেছি একটা real production issue থেকে । শুরুতে problem টা শেয়ার করছি। DigitalOcean এর বর্তমানে যে MCP server আছে সেটা local MCP । https://github.com/digitalocean-labs/mcp-digitalocean । কিন্ত MCP tooling এ remote MCP add করা more easier, আবার অনেক tool এর সাথে local MCP integrate করা যায় না , যেমন chatGPT এর সাথে remote MCP is suitable, local MCP is not ।
DigitalOcean এর practice হলো এখানে প্রচুর test লিখা থাকে , নানা ধরণের test । Remote MCP এর জন্য E2E (end-to-end) test লিখা আছে । যেটা আমাদের internal pipeline এ run করে । তবে test গুলো public আর চাইলে আপনি দেখতে পারবেন এখানে থেকে ।
তো remote MCP এর pipeline setup করার সময় দেখলাম যে CI fail করছে E2E এ । যদিও local এ, mac and linux দুইটাই test pass করছে ! Fail করছে কারণ খুবি basic একটা জিনিস overlook করে গিয়েছিলাম ( আমি ই ) ।
Failing CI and Where It’s Coming From
আমাদের internal pipeline এর একটা failed test । Error টা হলো
transport error: failed to send request: failed to send request: Post “http://localhost:32772/mcp”: read tcp [::1]:41406->[::1]:32772: read: connection reset by peerএখন error টা যদি খেয়াল করি, তাহলে দেখাচ্ছে যে localhost থেকে request করার পরে, request establish হচ্ছে কিন্তু পরে request টা close করে দেয়া হচ্ছে। Connection reset by peer বলতে এরকম না যে both party agree করেছে connection টা close করতে, it’s more like slamming the door !
মজার বিষয় হলো এটা local machine, mac and linux দুইটাতেই successfully run করে, কিন্তু github action এ করে না ! তো কি হচ্ছে ? অনেকেই হয়ত ধরতে পেরেছেন । যারা এখনো guess করছেন , তাদের জন্য hint
Post “http://localhost:32772/mcp”: read tcp [::1]:41406->[::1]:32772
Root Cause Analysis
এখানে প্রথমে client request করছে github action এ run করা একটি local container এ, localhost এর through তে । Connection establish হচ্ছে । তবে establish হয়েছে IPv6 ( `[::1]`। কারণ localhost এর through তে হওয়া তে একটা hostname resolution lookup হচ্ছে । যে IPv6 return করছে । যদিও আমরা explicitly IPv6এর জন্য request করিনি, তবুও IPv6 result পেয়েছি কারণ, go client আসলে by default ২ টা request send করে connection establish করার সময় । এটাকে DualStack boolean দিয়ে configure করা যেত1 । DualStack মানে একসাথে IPv4 + IPv6 dial চেষ্টা করা ( Happy Eyeballs algorithm ) ।
Aug 31, 2016 এ এই feature টি go এর net/http package এ add করা হয়েছিলো। https://go-review.googlesource.com/c/go/+/28077/3/src/net/http/transport.go ।
but the problem is, FakeWebsocketServer only IPv4 এ listen করছে digitalocean-labs/mcp-digitalocean/testing/e2e_websocket_test.go ।
আমরা যদি go এর `net/` package এর documentation দেখি, this is the following :
এখানে `a literal unspecified IP address` টা highlight note করছি, আমরা explicitly `0.0.0.0` দিয়ে IPv4 specify করছি । চেষ্টা করছি একটা sequence টা দেখাতে ।
আমরা কিন্তু connection failed শুরুতেই পাই নি । একটা connection establish হয়েছে, তারপরে stream read করার সময় fail করেছে । So, in short, আমাদের এখানে IPv6 আর IPv4 এর একটা mix and match হয়াতে আমরা linux kernel থেকে connection RST (reset) পাচ্ছি ।
Solution
এটার solution হিসাবে আমরা যদি IPv6 না IPv4 এ request করতে হবে, সেটা go এর ওপর ছেড়ে না দিয়ে নিজেরা control করে বলে দিতে পারি যে IPv4 এ request করতে, আমাদের problem solved । আই এই ছোট্ট PR https://github.com/digitalocean-labs/mcp-digitalocean/pull/195 । এই PR এর change গুলো simply localhost থেকে 127.0.0.1 তে change করে দেয়া ।
And আমাদের CI green.
localhost আর 127.0.0.1
আমরা অনেকেই localhost আর 127.0.0.1 interchangeably ব্যবহার করি । তবে এদের মাঝে fundamental একটা difference আছে । 127.0.0.1 একটা fixed IP address একটা বড় loopback range এ । 127.0.0.1 আসলে part of 127.0.0.0/8 . এটা specific IP হয়াতে টা এখানে DNS resolution বা hostname resolution লাগে না ।
On the other hand, localhost একটা hostname . Hostname হলো একটা human-readable label/name for network connected devices এর জন্য । যেমন, printer.local, raspberrypi.local etc. আমরা যদি localhost দিয়ে dial/request করি, তাহলে প্রথমে ঐ device এর জন্য IP address resolve হয়া লাগবে । Back to basics, OSI Networking layer 2.
যেহেতু এখানে DNS use হচ্ছে না, সেহেতু /etc/hosts file এ (on unix like systems) কিছু record থাকে, যেখানে বলা থাকে কোন hostname এর জন্য কোন IP. Mac এও same file.
আমরা যদি /etc/hosts file কে change করে say 127.0.0.1 IP করে দেই , তাহলে ঐ machine থেকে facebook.com এ গেলে এরকম unable to connect আসবে । আমার /etc/hosts file
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting. Do not change this entry.
##
127.0.0.1 localhost
127.0.0.1 facebook.com
::1 facebook.com # IPv6 এর জন্য
127.0.0.1 www.facebook.com
::1 www.facebook.com # www. subdomain এর জন্য, IPv6
আর running a ping command `ping facebook.com` ; এটাও fail করছে । If you note the IP for ping, 127.0.0.l !
আপনি যদি এটা try করে থাকেন আর still facebook.com এই resolve হতে থাকে, তার কারণ হলো DNS caching. dscacheutil নামের একটা program আছে যেটা আপনাকে resolve হয়া IP দেখাতে help করবে ।
dscacheutil -q host -a name facebook.com
name: facebook.com
ipv6_address: ::1
name: facebook.com
ip_address: 127.0.0.1pretty cool hah?
A fun fact
বেশ কয়েক বছর আগে এই technique কাজে লাগিয়ে Bangladesh gray hat hackers ( or rotating rotor যেটার admin ছিলো ) তারা 3CA ( 3xp1r3 Cyber Army) ‘র forum hack করেছে বলে দাবি করেছিলো, proof হিসাবে video দিয়েছিলো । কিন্তু আসলে 3CA forum এর hostname /etc/hosts এ edit করে local একটা file serve করে সেটার video upload করেছিলো যদ্দুর মনে পড়ে >_< ।
Docker and Container Networking
Containerized environment এ এটা understand করাটা important । অনেকেই এখানে mix and match করেন (আমিও করতাম একসময়), আর অনেক সময় যেত debug করতে ।
Both localhost আর 127.0.0.1 সবসময় current network namespace এর loopback interface কে refer করে । তবে, প্রতিটি container এর নিজের isolated network namespace আছে। ধরুন আমাদের একটা app run করছে
# From host machine:
docker run -p 8080:8080 projectlighthouse
# On the host, localhost/127.0.0.1 refer to the HOST namespace:
# Connects to host’s 127.0.0.1:8080 (port forwarded to container)
curl http://localhost:8080
# Same - connects to host’s loopback
curl http://127.0.0.1:8080
# Inside the container, localhost/127.0.0.1 refer to the CONTAINER namespace:
docker exec -it projectlighthouse bash
# Connects to service INSIDE this container only
curl http://localhost:8080
# Same - container’s loopback, NOT the host
curl http://127.0.0.1:8080 127.0.0.1 != 127.0.0.1
একটা important বিষয়, container এর 127.0.0.1 আর host machine এর 127.0.0.1 আলাদা । আলাদা namespace । আমরা container এর outside থেকে যে port forward করি (-p 8000:8000) , এটা একটা mapping তৈরি করে, কিন্তু এটা host এর network interface দিয়ে যাচ্ছে, loopback দিয়ে না ।
Conclusion
যদিও localhost আর 127.0.0.1 অনেক সময় interchangeably use করি আমরা, তবে তারা fundamentally different,
127.0.0.1 হলো concrete IPv4 address with guaranteed behavior
localhost হলো hostname, subject to resolution, configuration, and protocol negotiation
Production system এর জন্য, prefer explicit IP addresses in binding configurations. আর development, use 127.0.0.1 when you need predictable IPv4 behavior.
Production debugging এ এই দুইটা জিনিস এর difference understand করা can save you hours of troubleshooting of the mysterious connection failures… hopefully.
Just fyi, 2019 সালে আবার DualStack depreciated করে দেয়া হয়েছে https://go-review.googlesource.com/c/go/+/146659 , এখন by default enable করা থাকে ।











