Trưa 27/04/2005
Cả sáng nay lại bao nhiêu là công việc dồn dập. Tôi đang ở giai đoạn cao điểm của một số công trình ở công ty nên chẳng lấy được bao nhiêu thời gian để tiếp tục với mớ luật còn lại cho HVA forum. Mãi đến trưa, tôi lại tiếp tục vừa ăn trưa, vừa log vào server HVA để làm nốt phần dang dở.
Sau mười phút, tôi hoàn tất phần còn lại mà tôi bỏ dở tối qua. Kiểm tra lại một lần cuối tôi tháo bỏ phần ứng dụng trong .htaccess mà JAL đặt vào tối hôm qua để bắt người dùng phải đăng nhập. Sau khi restart lại apache, tôi phóng ngay một "tail" trên console để theo dõi log của web server, tôi thích thú khi thấy 100% các cú GET và POST kỳ quặc kia bị khựng lại và được "wwwect" về lại trang index.html nếu chúng không thoả mãn
các điều kiện áp đặt. Tôi thở phào nhẹ nhõm khi thấy mớ luật tuy chưa được "debug" một cách trọn vẹn nhưng làm việc khá tốt. Tôi biết chắc đâu đó vẫn còn những điểm phải điểm xuyết; nhưng trước mắt, tình hình có vẻ sáng sủa hơn rất nhiều. Server load giữ ở mức y hệt như khi "ép" các request xuyên qua cổng 443.
Vài phút trôi qua, nỗi lo ngại mà tôi đoán trước từ từ lộ rõ. Bạn còn nhớ tôi nhận định thế này:
"Trong khi thao tác các bước cần thiết để tạo thêm lớp "vỏ" này, tôi đã ngầm e ngại một "triệu chứng phụ" sẽ xảy ra. Triệu chứng này không thể làm cho máy chủ HVA treo vì cạn kiệt tài nguyên nhưng lại có thể không cho người dùng truy cập vào diễn đàn HVA." Tôi nhận thấy vận tốc truy cập đến HVA càng lúc càng chậm mặc dù server load vẫn rất thấp, vẫn giữ ở mức không quá 2.0. Tôi đoán ra được chuyện gì nhưng muốn kiểm chứng. Một lệnh sau đã xác thực thắc mắc của tôi:
Code: # ps -ef | grep httpd | wc -l
1386
Con số trên cho thấy điều gì? Nó cho thấy rằng apache cực kỳ bận rộn. Hiện apache đã tạo ra đến 1386 processes (vì HVA dùng prefork model cho apache để chạy với php). Cũng may là tôi đã "lo xa" khi biên dịch lại apache, giá trị "MaxClients" đã được sửa thành 4096, tức là gấp 16 lần con số mặc định (và được apache "hard coded" trong mã nguồn). MaxClients vẫn còn dư để phục vụ cho requests nhưng các request lại chậm vì sao? Tôi đoán rằng apache phải tạo ra các child process liên tục nhằm đáp ứng tình trạng đang bị DoS căng thẳng này. Có ba yếu tố chính trong cấu hình của apache khiến cho con số "MaxClients" này gia tăng nhanh chóng:
- MinSpareServer là 2
- MaxRequestPerChild là 500
- KeepAliveTimeOut là 15
Cứ mỗi khi một ChildProcess đã phục vụ xong 500 request, nó tự động bị hủy và một ChildProcess khác được tạo ra (để tránh tình trạng bị memory leak). Khi một ChildProcess mới được tạo ra, thêm 2 ChildProcess dự phòng khác cũng được hình thành vì nếu không có sẵn, lúc ấy apache phải tạo nó để phục vụ. Trong khi ChildProcess được tạo, apache không tiếp nhận request. Rõ ràng con số 500 cho MaxRequestPerChild không thích hợp vì trong tình trạng bị DoS thế này, chẳng mấy chốc sẽ đụng tới giới hạn này và công tác tạo ChildProcess mới lại xảy ra. Sở dĩ con số này rất lớn là vì tôi ấn định MinSpareServer nhưng không ấn định MaxSpareServer vì tôi thấy rằng để cho apache tự động điều tác việc này tốt hơn, apache biết rõ nó cần có bao nhiêu ChildProcess dự phòng tuỳ tình hình. Có lẽ trong tình trạng nhận request dồn dập này, apache đã tạo ra tối đa vài chục "spare" nên con số 1386 không có gì đáng ngạc nhiên. Con số KeepAliveTimeOut có giá trị là 15 giây quả là thoải mái và nó tạo hiệu xuất cho hoàn cảnh bình thường (không bị DoS) nhưng trong hoàn cảnh hiện tại, nó kéo dài thời gian socket ở tình trạng TIME_WAIT vì cần bảo đảm dữ liệu từ mỗi đầu truy cập đã hoàn toàn chấm dứt.
Đối với một webserver bình thường, không bị DoS, giá trị ấn định trên khá thích hợp vì nó bảo đảm không bị memory leak, nó luôn luôn có ít nhất là 2 ChildProcess dự phòng để liên tục nhận request. Tuy nhiên, rõ ràng nó không còn ứng hiệu trong tình trạng DoS "hợp lệ" này. Tôi không muốn các ChildProcess được tạo ra quá nhanh và quá nhiều, bởi thế, con số MaxRequestPerChild được chỉnh thành 20000.
Tôi restart lại apache và tiếp tục theo dõi tình hình. Quả có cải thiện nhưng vẫn chưa thật sự nâng cao tốc độ truy cập đến mức bình thường. Tôi chạy liển hai lệnh để xem tình hình các sockets thế nào:
Code: # netstat -nat | grep WAIT | wc -l
13079
Ôi chao! Chưa từng thấy trên máy chủ HVA bao giờ. Lý do tại sao nhiều TIME_WAIT và FIN_WAIT đến thế? Thế này, khi "x-flash" còn nhận diện được, phần lớn chúng đã bị "triệt" một cách im ắng từ IP layer. Số còn lại có đụng phải mod_security cũng không nhiều đến nỗi phải dồn đống như thế. Hơn nữa trận tấn công này hình như nặng nề và dồn dập hơn trước.
Tôi thử thêm một cách khá nhanh và.... "dơ" để "bắt" những gói SYN đi đến cổng dịch vụ HTTP
-62-:
Code: /usr/sbin/tcpdump tcp[13] == 2 and port 80 > numsyn
Tôi cho lệnh này chạy chừng một phút rồi ngừng. Mở hồ sơ "numsyn" này ra bằng
vi, tôi nhanh chóng xác định trong 60 giây qua có bao nhiêu cú SYN đi vào. Lúc này HVA nhận chừng trên 2000 cú SYN một giây vì hồ sơ "numsyn" ở trên cho tôi biết có gần 130 ngàn dòng được lưu trong 1 phút vừa qua. Điều tôi có thể xác thực 100% là máy chủ HVA bị vướng trong tình trạng nhận quá nhiều requests và tạo quá nhiều sockets nhưng không thể giải toả chúng kịp thời và nhịp nhàng. Nếu apache dùng giá trị mặc định cho MaxClients là 256 thì có lẽ lúc này chẳng có ai có thể vào diễn đàn. Vậy, điều cần phải giải quyết hiển nhiên là giải toả mớ sockets đang dồn đống kia. Không may là giờ ăn trưa của tôi đã hết từ lâu. Tôi không thể tiếp tục ngồi táy máy với HVA server vì hàng đống công việc đang chờ đợi tôi. Tôi lẩm bẩm
"thôi để chiều nay hoặc tối nay xem sao" và logoff HVA server.
Chiều 27/04/2005
Tôi vừa xong một cuộc họp ở công ty. Có chỉ thị tạm ngưng công trình vì cần phải điều chỉnh lại một số yêu cầu từ đám kinh tế. Điều này có nghĩa là tôi có thêm ít thời gian chiều nay để tiếp tục táy máy với máy chủ HVA. Sau khi thu xếp xong giấy tờ và cập nhật thông tin, dữ kiện cho công trình, tôi tiếp tục bắt tay vào việc khắc phục tình trạng socket dồn ứ trên máy chủ HVA.
Log vào HVA server, tôi thấy tình hình y hệt trưa hôm nay, có nghĩa là các cú "x-flash" vẫn đổ dồn vào bốn vị trí:
- GET /
- POST /
- GET /forum/
- POST /forum/index.php
và số lượng TIME_WAIT + FIN_WAIT vẫn dồn đống. Tất nhiên là thế vì chưa hề có thay đổi xác đáng nào để khắc phục tình trạng này. Tôi mở cấu hình của apache ra xem lại và ngạc nhiên khi thấy giá trị KeepAliveTimeOut vẫn còn là 15 giây. Có lẽ lúc trưa tôi đãng trí và chưa điều chỉnh nó. Tôi sửa ngay giá trị này thành 5 giây. Stop apache, đợi cho mọi socket trên máy chủ hoàn toàn chấm dứt (không còn TIME_WAIT và FIN_WAIT nào nữa), rồi mới start apache.
Cứ mỗi phút tôi "đo" số lượng "WAIT" socket một lần và sau năm phút, số lượng "WAIT" socket nằm ở vị trí trên dưới 3000 và giữ lại ở mức độ này:
Code: # netstat -nat | grep WAIT | wc -l
3350
Theo tôi, bấy nhiêu đã là một cải thiện lớn lao. 5 giây để duy trì socket ở tình trạng "WAIT" bảo đảm thông tin ở hai đầu truy cập (giữa client và apache server trên máy chủ HVA) hoàn toàn kết thúc để tránh tình trạng mất dữ liệu theo tôi là quá đủ. Lý do tôi dùng giá trị này vì tôi chưa hề thấy thời gian chuyển gởi thông tin giữa máy chủ HVA và trình duyệt nào đó quá 3 giây (dựa trên thông tin tôi sniff và thử nghiệm). Thay đổi này đã cắt bỏ một số lượng rất lớn các "WAIT" socket làm đình trệ máy chủ. Tuy nhiên, tôi vẫn chưa hoàn toàn hài lòng vì con số trên dưới 3000 kia có thể gia tăng dễ dàng nếu số lượng x-flash gia tăng.
Tôi quyết định làm một thử nghiệm nhỏ bằng cách chạy 2 sniffers một lượt (dùng tcpdump trong trường hợp này), một cái chạy trên máy chủ HVA, một cái chạy trên chính proxy server tôi dùng để truy cập HVA (bạn còn nhớ tôi dùng ssh tunnel để truy cập từ sở về nhà và dùng proxy server ở nhà mà tôi đề cập trong một bài ký sự nào đó trước đây?). Thêm một console nối vào HVA server để theo dõi giá trị trên /proc/net/ip_conntrack
-63-. Cả 2 sniffer này đều sniff cổng 80 và sniff host IP chính là IP của proxy server tôi dùng. Khi mọi việc đã chuẩn bị xong, tôi dùng Firefox để duyệt HVA một cách bình thường (như một người dùng bình thường), có nghĩa là bao gồm bước logon, và duyệt xuyên dăm ba topic.... Sau vài phút, tôi ngừng cả 2 sniffer và thu thập cả hai hồ sơ được sniff trên 2 vị trí.
Mang chúng về laptop, tôi bắt tay vào phân tích (dùng Ethereal). Điều tôi cần phân tích ở đây không phải là vận tốc truy cập mà tôi muốn tìm hiểu giữa server và client mất bao lâu để triệt tiêu socket sau khi cuộc truy cập hoàn tất. Tôi cần 2 hồ sơ từ hai phía để đối chiếu và nắm chắc là chúng hoàn toàn ăn khớp. Điều quan trọng tôi thấy được từ các mảnh packets được sniff ở trên là: từ lúc HVA server gởi gói tin có mang FIN bit (để báo cho trình duyệt của tôi là dữ liệu sau gói tin này là hết, không còn gì để cung cấp nữa) cho đến khi trình duyệt tôi dùng tiếp nhận dấu hiệu FIN này bằng cách trả lời FIN-ACK chỉ mất chưa tới 1 giây. Sau đó, socket trên HVA server đi vào tình trạng "TIME_WAIT" và duy trì tình trạng này
khá lâu trước khi đi vào tình trạng CLOSED. Đây chính là nguồn gốc của hàng đống "WAIT" socket nằm trên HVA server.
Để kiểm nghiệm, tôi thử duyệt HVA bằng FireFox một lần nữa và theo dõi thông tin cụ thể IP của proxy server tôi dùng trên
/proc/net/ip_conntrack lẫn thông tin trên
netstat lấy được trên HVA server. Quả thật, trọn bộ giai đoạn thiết lập connection cho đến khi connection kết thúc ứng hiệu với giá trị KeepAliveTimeOut trên apache. Tuy nhiên, "connection tracking table" của netfilter "giữ riệt" socket này ở dạng "TIME_WAIT" đến những 120 giây trước khi hủy bỏ nó. Thật ra sự áp đặt này của "conntrack table" của netfilter chẳng có gì sai quấy cả. Dụng đích của nó là chờ để bảo đảm connection phía client (trình duyệt) hoàn toàn chấm dứt để tránh tình trạng "treo". Không may, ở tình trạng bị DoS dồn dập, cơ chế bảo đảm các "state" của một tcp connection lại tạo đình trệ và ảnh hưởng không tốt đến máy chủ.
Tôi vào
/proc/sys/net/ipv4/netfilter/ và điều chỉnh trọn bộ giá trị ở đây. Cắt giảm thời gian "đợi" từ 1/2 đến 2/3 thời gian quy định theo mặc định. Nên nhớ, các giá trị này được ứng hiệu "on the fly" (ngay lập tức mà không cần stop / start gì cả). Một phút sau, số lượng "TIME_WAIT" trở thành:
Code: # netstat -nat | grep WAIT | wc -l
983
Thêm 2/3 các "WAIT" sockets được giảm thiểu! Ngay lúc này, duyệt diễn đàn HVA nhanh hơn rõ. Tôi không dám mong đạt được ở mức bình thường vì thật sự HVA đang bị DoS dồn dập; có lẽ không nên ngớ ngẩn mà mong đợi kết quả không tưởng như thế. Vậy, việc điều chỉnh các giá trị trên hồ sơ cấu hình của apache và kernel dính dáng gì đến trang index.html được áp đặt kia?
"tôi chẳng thấy sự tương quan rõ rệt chỗ nào cả!", bạn có thể hỏi câu này. Thật sự chúng liên quan trực tiếp và chặt chẽ với nhau. Ứng dụng "lọc" mọi request không thoả mãn quy định sẽ "bị" quay về lại index.html đột nhiên gia tăng công việc ở tầng application cũng như gia tăng số lượng sockets. Thay vì các gói tin vi phạm được nhận diện và triệt tiêu ở ngay tầng IP (và bởi thế không có tình trạng các "WAIT" socket nằm vất vưởng), các gói tin "vi phạm" được cản lọc trên tầng application vì chúng chẳng có đặc điểm "vi phạm" nào cả. Những chỉnh lý trên cấu hình apache và trên kernel ở đây đều phục vụ cho một mục đích duy nhất:
giải phóng các sockets càng nhanh càng tốt để có thể tiếp tục phục vụ.
Tôi log vào YIM và gởi cho JAL một thông điệp:
"bồ duyệt thử HVA xem sao?". Một phút sau, JAL hồi báo:
"Ngon lắm rồi đó lão, chạy vù vù!". Tôi đáp:
"vậy thì tớ dzọt đây!" và tôi logoff.
Tối 27/04/2005:
Tối nay, khi duyệt diễn đàn HVA, tôi va vào một trở ngại khác. Tôi vào HVA được nhưng gặp tình trạng khựng lại trên trình duyệt và thỉnh thoảng tôi phải nhấn nút "Stop" rồi thử bấm trên đường dẫn tôi muốn duyệt thì trang web mới load. Tôi không tin đây là trục trặc trên trình duyệt của tôi mà có lẽ là một vấn đề "bí ẩn" nào đó trên máy chủ HVA.
Log vào máy chủ HVA xuyên qua SSH (một cách dễ dàng), tôi nhận thấy ngay là server load rất thấp, số lượng ChildProcess của apache đang chạy cũng ở mức chấp nhận được (dù đang bị DoS), số lượng "WAIT" sockets nằm ở mức trên dưới 1000. Đo thử số lượng SYN mà máy chủ HVA nhận (và tạo) trong một giây, tôi thấy con số này gia tăng lên khoảng gần 4000 SYN / 1 giây. Kiểm tra thử số lượng ESTABLISHED socket là bao nhiêu, tôi thấy nó liên tục nằm ở mức 127 - 128. Một ý nghĩ loé nhanh trong đầu, tôi kiểm tra ngay một giá trị cực kỳ quan trọng:
Code: # cat /proc/sys/net/core/somaxconn
128
và rồi:
Code: # netstat -nat | grep SYN
128
Điều này chứng tỏ gì đây nhỉ? Máy chủ HVA đã dùng tối đa số lượng socket tiếp nhận SYN cho phép. Lạ nhỉ? HVA firewall có phần cản lọc SYN FLOOD cơ mà?
. Đúng là như vậy nhưng phần cản lọc này trở nên khá hạn chế khi HVA server "bị" trên 500 IP cùng "rải" ít nhất 1 đến 5 cái SYN cho mỗi giây. Nếu các IP này "rải" với tầng số như vậy thì hẳn HVA firewall cho vào vì chúng không mang dấu hiệu SYN FLOOD ở đâu cả. Cứ cho rằng có 500 IP chỉ gởi 2 request cho mỗi giây vào HVA server, và nếu phía client trả lời nhanh chóng sau khi HVA server trả lời SYN-ACK thì cũng mất 1/5 giây thì:
1000 SYN * 0.2 giây = 200 syn cho mỗi giây
Lúc nào listen()
-64- cũng bị quá giới hạn:
200 - 128 = 72 SYN (đứng chờ)
thì tất nhiên khi người dùng duyệt HVA phải bị chậm hoặc khựng lại. Một khi xuất truy cập đã hình thành thì thông tin được chuyển tải rất nhanh bởi vì xuất truy cập này đã ở tình trạng "ESTABLISHED" trên tcp.
Vậy
somaxconn dính dự gì đến listen()? Hẳn nhiên rồi, và còn dính trực tiếp nữa là đằng khác.
somaxconn chính là int thứ ba của hàm listen() (đã chú thích ở
-64-). Hiện tại, giá trị
somaxconn trên HVA server rất thấp và chắc chắn không đủ để phục vụ các request (kể cả hợp lệ lẫn "bất hợp lệ"). Cứ 128 SYN gởi vào thì có lẽ hơn 99% là các cú "x-flash" tham lam kia chiếm lấy, những request hợp lệ của người dùng bình thường hẳn phải đi vào hàng đợi để tuần tự được truy cập --> chậm. Nếu đợi lâu --> timeout, trình duyệt sẽ được báo lỗi là "host is not contactible" hay đại loại như vậy.
Giải pháp? tất nhiên là phải gia tăng giá trị
somaxconn. Không những thế, giá trị "syn_backlog" cũng phải được gia tăng (đến mức độ thích hợp mà lượng memory trên hệ thống cho phép) với mục đích:
- gia tăng cơ hội (tỉ số) người dùng hợp lệ được phép truy cập.
- gia tăng thời gian cho phép các request "bị" đưa vào hàng đợi "backlog" có điều kiện đợi mà không bị timeout (và nhận error).
Bạn có thể hỏi:
"tại sao trước giờ HVA bị DDoS sao không bị trường hợp này mà mãi đến bây giờ mới phát hiện?". Câu trả lời thế này: chuyện này đã xảy ra trước đây nhưng lại tái diễn vì hai lý do:
1. các giá trị ứng dụng và được điều chỉnh trên kernel thực hiện trên máy chủ cũ (cấu hình kém hơn) trước đây đã không được mang qua máy chủ mới một cách trọn vẹn.
2. khi mang qua, một số giá trị quan trọng đã không ứng dụng toàn thời trong /etc/sysctl.conf mà chỉ ứng dụng ở dạng:
# cat <n> > /proc/sys/net/ipv4/whatever_param
loại thay đổi "ad-hoc" này sẽ bị xoá mất và giá trị mặc định sẽ được dùng sau khi server được reboot.
Phải nói rằng, đây là một chểnh mảng của tôi. Chểnh mảng ở chỗ, tôi nghĩ rằng nó đã được ứng dụng và không buồn xem lại và đợi đến khi lâm vào tình trạng khó khăn rồi mới đi xuyên qua các bước thử nghiệm để tìm lý do.
Sau khi ấn định lại giá trị
somaxconn gấp ba lần giá trị trước đây và xem xét kỹ lưỡng các giá trị khác trên kernel. Tôi tạm ngưng apache và khởi động lại nó sau vài giây. Bạn đã đoán ra: truy cập HVA nhanh hơn và dễ dàng hơn cho dù vẫn còn đang bị DoS.
Giá phải trả cho chuyện này là
"lão JAL phải mua thêm RAM" (j/k JAL, RAM trên server còn dư dùng, nhưng nếu được thì nên chuẩn bị thêm một server nữa nếu DDoS biến thái ).
Chú thích:
-62- tcpdump cho phép "bắt" những gói tin ở dạng cụ thể dựa trên bit nào được set trên tcp flags. Nếu sử dụng nó đúng mức, tcpdump là một công cụ đa năng và không thể thiếu trong việc bắt và phân tích các gói tin. Xem thêm chi tiết các chọn lựa cho tcpdump bằng man page của nó:
$ man tcpdump.
-63- /proc/net/ip_conntrack giá trị biểu thị tình trạng (state) của các connection đang được netfilter theo dõi trên hệ thống. Muốn biết thêm chi tiết, xin tham khảo các tài liệu liên hệ trên website
http://www.iptables.org
-64- listen() queue. Để tiếp nhận một connection, socket phải được tạo ra và phải ở tình trạng sẵn sàng tiếp nhận truy cập và hàm listen() được dùng để áp đặt giới hạn bao nhiêu truy cập dịch vụ có thể cung cấp. Hàm listen() còn có thêm một tham số khác trực tiếp giới hạn số lượng "backlog connection", có nghĩa là nó quy định con số bao nhiêu connection được đứng trong hàng đợi để chờ lượt mình truy cập. listen() có hai tham số:
int listen(int s, int backlog);, trong đó s là integer ấn định giới hạn bao nhiêu connection được cho phép và
backlog là integer ấn định giới hạn "hàng đợi" chứa bao nhiêu request chờ được tiếp nhận.
Xem thêm
man socket, man listen và các tài liệu cụ thể cho socket programming trên Linux để nắm thêm chi tiết.