簡體   English   中英

TCP校驗和計算與wireshark計算不匹配

[英]TCP Checksum calculation doesn't match with the wireshark calculation

我遇到了示例程序生成的 tcp 校驗和(復制如下)與wireshark 計算的校驗和不匹配的問題。 有人可以指出我哪里出錯了。 在這里我嘗試了兩種方法

  1. tcp_checksum
  2. get_ipv6_udptcp_checksum。

有了這兩個,得到兩個不同的值,並且都與wireshark值不匹配。

我在這里復制 IP 和 TCP 標頭詳細信息。

IP 標頭:

0000 60 00 00 00 00 2a 06 80 10 80 a2 b1 00 00 00 00

0010 00 00 00 00 00 1e 00 00 ff 00 00 00 00 00 00 00

0020 00 00 00 00 00 00 00 24

TCP 標頭:

0000 04 22 00 50 00 01 e0 dd 00 01 42 74 50 14 22 38

0010 EB 10 00 00

我的理解是,添加偽標頭和 TCP 標頭值將給出校驗和。 手動添加值會給出完全不同的值。 當我嘗試以編程方式時,它是(38 eb)。 wireshark 顯示正確的值應該是0xb348

我哪里做錯了? 有人可以建議我如何手動完成嗎?

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>           // close()
#include <string.h>           // strcpy, memset(), and memcpy()

#include <netdb.h>            // struct addrinfo
#include <sys/types.h>        // needed for socket(), uint8_t, uint16_t
#include <sys/socket.h>       // needed for socket()
#include <netinet/in.h>       // IPPROTO_TCP, INET6_ADDRSTRLEN
#include <netinet/ip.h>       // IP_MAXPACKET (which is 65535)
#include <netinet/ip6.h>      // struct ip6_hdr
#define __FAVOR_BSD           // Use BSD format of tcp header
#include <netinet/tcp.h>      // struct tcphdr
#include <arpa/inet.h>        // inet_pton() and inet_ntop()
#include <sys/ioctl.h>        // macro ioctl is defined
#include <bits/ioctls.h>      // defines values for argument "request" of ioctl.
#include <net/if.h>           // struct ifreq
#include <linux/if_ether.h>   // ETH_P_IP = 0x0800, ETH_P_IPV6 = 0x86DD
#include <linux/if_packet.h>  // struct sockaddr_ll (see man 7 packet)
#include <net/ethernet.h>

#include <errno.h>            // errno, perror()
void ipv6_to_str_unexpanded(char *str, const struct in6_addr * addr) {
   sprintf(str, "%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x",
                 (int)addr->s6_addr[0], (int)addr->s6_addr[1],
                 (int)addr->s6_addr[2], (int)addr->s6_addr[3],
                 (int)addr->s6_addr[4], (int)addr->s6_addr[5],
                 (int)addr->s6_addr[6], (int)addr->s6_addr[7],
                 (int)addr->s6_addr[8], (int)addr->s6_addr[9],
                 (int)addr->s6_addr[10], (int)addr->s6_addr[11],
                 (int)addr->s6_addr[12], (int)addr->s6_addr[13],
                 (int)addr->s6_addr[14], (int)addr->s6_addr[15]);
printf("addr:[%s]\n",str);
}


static inline uint16_t
get_16b_sum(uint16_t *ptr16, uint32_t nr)
{
        uint32_t sum = 0;
        while (nr > 1)
        {
                sum +=*ptr16;
                nr -= sizeof(uint16_t);
                ptr16++;
                if (sum > UINT16_MAX)
                        sum -= UINT16_MAX;
        }

        /* If length is in odd bytes */
        if (nr)
                sum += *((uint8_t*)ptr16);

        sum = ((sum & 0xffff0000) >> 16) + (sum & 0xffff);
        sum &= 0x0ffff;
        return (uint16_t)sum;
}

static inline uint16_t 
get_ipv6_psd_sum (struct ip6_hdr * ip_hdr)
{
        /* Pseudo Header for IPv6/UDP/TCP checksum */
        union ipv6_psd_header {
                struct {
                        uint8_t src_addr[16]; /* IP address of source host. */
                        uint8_t dst_addr[16]; /* IP address of destination host(s). */
                        uint32_t len;         /* L4 length. */
                        uint32_t proto;       /* L4 protocol - top 3 bytes must be zero */
                } __attribute__((__packed__));

                uint16_t u16_arr[0]; /* allow use as 16-bit values with safe aliasing */
        } psd_hdr;

        memcpy(&psd_hdr.src_addr, &ip_hdr->ip6_src,
                        (sizeof(ip_hdr->ip6_src) + sizeof(ip_hdr->ip6_dst)));
        //psd_hdr.len       = ip_hdr->payload_len;
        psd_hdr.len       = ip_hdr->ip6_plen;
        psd_hdr.proto     = IPPROTO_TCP;//(ip_hdr->proto << 24);

        return get_16b_sum(psd_hdr.u16_arr, sizeof(psd_hdr));
}


static inline uint16_t 
get_ipv6_udptcp_checksum(struct ip6_hdr *ipv6_hdr, uint16_t *l4_hdr)
{
        uint32_t cksum;
        uint32_t l4_len;

        l4_len = (ipv6_hdr->ip6_plen);

        cksum = get_16b_sum(l4_hdr, l4_len);
        cksum += get_ipv6_psd_sum(ipv6_hdr);

        cksum = ((cksum & 0xffff0000) >> 16) + (cksum & 0xffff);
        cksum = (~cksum) & 0xffff;
        if (cksum == 0)
                cksum = 0xffff;

        return (uint16_t)cksum;
}

//! \brief Calculate the TCP checksum.
//! \param buff The TCP packet.
//! \param len The size of the TCP packet.
//! \param src_addr The IP source address (in network format).
//! \param dest_addr The IP destination address (in network format).
//! \return The result of the checksum.
uint16_t tcp_checksum(const void *buff, size_t len, struct in6_addr src_addr, struct in6_addr dest_addr)
{
    const uint16_t *buf=buff;
    uint16_t *ip_src=(void *)&src_addr, *ip_dst=(void *)&dest_addr;
    uint32_t sum;
    size_t length=len;

    // Calculate the sum                                            //
    sum = 0;
    while (len > 1)
    {
        sum += *buf++;
        if (sum & 0x80000000)
            sum = (sum & 0xFFFF) + (sum >> 16);
        len -= 2;
    }

    if ( len & 1 )
    // Add the padding if the packet lenght is odd          //
    sum += *((uint8_t *)buf);

    // Add the pseudo-header                                        //
    sum += *(ip_src++);
    sum += *ip_src;
    sum += *(ip_dst++);
    sum += *ip_dst;
    sum += htons(IPPROTO_TCP);
    sum += htons(length);

    // Add the carries                                              //
    while (sum >> 16)
        sum = (sum & 0xFFFF) + (sum >> 16);

    // Return the one's complement of sum                           //
    return ( (uint16_t)(~sum)  );
}

// Define some constants.
#define ETH_HDRLEN 14  // Ethernet header length
#define IP6_HDRLEN 40  // IPv6 header length
#define TCP_HDRLEN 20  // TCP header length, excludes options data

// Function prototypes
uint16_t checksum (uint16_t *, int);
uint16_t tcp6_checksum (struct ip6_hdr, struct tcphdr, uint8_t *, int);
char *allocate_strmem (int);
uint8_t *allocate_ustrmem (int);
int *allocate_intmem (int);

int
main (int argc, char **argv)
{
  int i, status, frame_length, sd, bytes, *tcp_flags, opt_len;
  char *interface, *target, *src_ip, *dst_ip;
  struct ip6_hdr iphdr;
  struct tcphdr tcphdr;
  uint8_t *src_mac, *dst_mac, *ether_frame;
  uint8_t *options;
  struct addrinfo hints, *res;
  struct sockaddr_in6 *ipv6;
  struct sockaddr_ll device;
  struct ifreq ifr;
  void *tmp;

  // Allocate memory for various arrays.
  src_mac = allocate_ustrmem (6);
  dst_mac = allocate_ustrmem (6);
  ether_frame = allocate_ustrmem (IP_MAXPACKET);
  interface = allocate_strmem (40);
  target = allocate_strmem (INET6_ADDRSTRLEN);
  src_ip = allocate_strmem (INET6_ADDRSTRLEN);
  dst_ip = allocate_strmem (INET6_ADDRSTRLEN);
  tcp_flags = allocate_intmem (8);
  options = allocate_ustrmem (40);

  // Interface to send packet through.
  strcpy (interface, "eth0");

  // Source IPv6 address: you need to fill this out
  strcpy (src_ip,"1080:a2b1::1e:0");
  strcpy (dst_ip,"ff00::24");

  // IPv6 header

  // IPv6 version (4 bits), Traffic class (8 bits), Flow label (20 bits)
  iphdr.ip6_flow = htonl ((6 << 28) | (0 << 20) | 0);

  // Payload length (16 bits): TCP header + TCP options
  //iphdr.ip6_plen = htons (TCP_HDRLEN + opt_len);
  //iphdr.ip6_plen = htons (TCP_HDRLEN);
  iphdr.ip6_plen = htons(TCP_HDRLEN);

  // Next header (8 bits): 6 for TCP
  iphdr.ip6_nxt = IPPROTO_TCP;

  // Hop limit (8 bits): default to maximum value
  iphdr.ip6_hops = 128;

  // Source IPv6 address (128 bits)
  if ((status = inet_pton (AF_INET6, src_ip, &(iphdr.ip6_src))) != 1) {
    fprintf (stderr, "inet_pton() failed.\nError message: %s", strerror (status));
    exit (EXIT_FAILURE);
  }

char srcAddr[32];
memset(srcAddr,0,32);
printf("src ip addr:");
ipv6_to_str_unexpanded(srcAddr,&(iphdr.ip6_src));


  // Destination IPv6 address (128 bits)
  if ((status = inet_pton (AF_INET6, dst_ip, &(iphdr.ip6_dst))) != 1) {
    fprintf (stderr, "inet_pton() failed.\nError message: %s", strerror (status));
    exit (EXIT_FAILURE);
  }
char dstAddr[32];
memset(dstAddr,0,32);
printf("dst ip addr:");
ipv6_to_str_unexpanded(dstAddr,&(iphdr.ip6_dst));


  // TCP header

  // Source port number (16 bits)
  tcphdr.th_sport = htons (1058);
printf("src port:[%d]\n",tcphdr.th_sport);

  // Destination port number (16 bits)
  tcphdr.th_dport = htons (80);

  // Sequence number (32 bits)
  tcphdr.th_seq = htonl (1);
printf("seq :[%d]\n",tcphdr.th_seq);

  // Acknowledgement number (32 bits): 0 in first packet of SYN/ACK process
  tcphdr.th_ack = htonl (1);

  // Reserved (4 bits): should be 0
  tcphdr.th_x2 = 0;

  // Data offset (4 bits): size of TCP header + length of options, in 32-bit words
  //tcphdr.th_off = (TCP_HDRLEN + opt_len) / 4;
  tcphdr.th_off = TCP_HDRLEN/4;

  // Flags (8 bits)

  // FIN flag (1 bit)
  tcp_flags[0] = 0;

  // SYN flag (1 bit): set to 1
  tcp_flags[1] = 0;

  // RST flag (1 bit)
  tcp_flags[2] = 1;

  // PSH flag (1 bit)
  tcp_flags[3] = 0;

  // ACK flag (1 bit)
  tcp_flags[4] = 1;

  // URG flag (1 bit)
  tcp_flags[5] = 0;

  // ECE flag (1 bit)
  tcp_flags[6] = 0;

  // CWR flag (1 bit)
  tcp_flags[7] = 0;

  tcphdr.th_flags = 0;
  for (i=0; i<8; i++) {
    tcphdr.th_flags += (tcp_flags[i] << i);
  }

  // Window size (16 bits)
  tcphdr.th_win = htons (8760);

  // Urgent pointer (16 bits): 0 (only valid if URG flag is set)
  tcphdr.th_urp = htons (0);

  tcphdr.th_sum  = 0;
  //tcphdr.th_sum  = get_ipv6_udptcp_checksum(&iphdr, (uint16_t *)&tcphdr);
  tcphdr.th_sum = tcp_checksum((void *)&tcphdr, htons(20), iphdr.ip6_src, iphdr.ip6_dst);

printf("TCP Checksum:[%x]\n",tcphdr.th_sum);

return 0;
}

char *
allocate_strmem (int len)
{
  void *tmp;

  if (len <= 0) {
    fprintf (stderr, "ERROR: Cannot allocate memory because len = %i in allocate_strmem().\n", len);
    exit (EXIT_FAILURE);
  }

  tmp = (char *) malloc (len * sizeof (char));
  if (tmp != NULL) {
    memset (tmp, 0, len * sizeof (char));
    return (tmp);
  } else {
    fprintf (stderr, "ERROR: Cannot allocate memory for array allocate_strmem().\n");
    exit (EXIT_FAILURE);
  }
}

手動校驗和計算

TCP/UDP/IP 的校驗和計算相當簡單。 所謂的“16 位補碼”算法只是一個概念,即在兩個 16 位數字相加期間,16 位上的任何內容都從第 0 位加回來。例如

0x8000 + 0x8000 = 0x10000 => 0x1 + 0x0000 = 0x0001.

該算法的特性之一是通過簡單的二進制反轉產生負值。 這個算法中的 0 有 2 個二進制值:0x0000 和 0xffff

-0x0001 = ~0x0001 = 0xfffe;
0xfffe + 0x8000 + 0x8000 = 0x1fffe => 0x1 + 0xfffe = 0xffff = 0x0000

關於 16 位補碼的另一個好處是,您在進行 16 位加法時不必擔心字節順序,您只需正確轉換最終結果。 發生這種情況是因為進位位總是從一個字節傳輸到另一個字節並且永遠不會丟失。 下面是在 little endian 機器中讀取數據的相同示例:

0x0080 + 0x0080 = 0x0100 => htons(0x0100) = 0x0001

這就是為什么所有校驗和計算算法都不會將每個 16 位值從網絡轉換為主機字節順序的原因。

考慮到所有這些,您只需將數據塊分解為 16 位作品,以常規方式將它們加在一起,然后將高 16 位添加到低 16 位並在將其寫回數據包之前反轉結果。

在您的示例中,TCP 標頭校驗和將計算為:

0x0422 + 0x0050 + 0x0001 + 0xe0dd + 0x0001 + 0x4274 + 0x5014 + 0x2238 +
0x0000 + 0x0000 = 0x19a11 = 0x1 + 0x9a11 = 0x9a12
^^^^^^ // <- this is the place for the TCP checksum

TCP 校驗和計算中所述,您需要向 TCP 數據包添加偽標頭,以便源和目標 IP 地址和端口也參與校驗和計算。 這個偽頭對於 IPv4 和 IPv6 是不同的。 在您的 IPv6 示例中,它將是:

0x1080 + 0xa2b1 + 0x0000 + 0x0000 + // source IPv6 address
0x0000 + 0x0000 + 0x001e + 0x0000 +
0xff00 + 0x0000 + 0x0000 + 0x0000 + // destination IPv6 address
0x0000 + 0x0000 + 0x0000 + 0x0024 +
0x0016 +                            // IP payload (TCP packet) lenght
0x0006                              // Next Header value for TCP
= 0x1b28f = 0x1 + 0xb28f = 0xb290

現在 TCP 和 IP 偽標頭校驗和組合將是:

0x9a12 + 0xb290 = 0x14ca2 = 0x1 + 0x4ca2 = 0x4ca3

在將校驗和寫回標頭之前否定校驗和:

~0x4ca3 = 0xb35c

注意:這個校驗和仍然與您聲稱的 Wireshark 計算的不同,主要是因為您提供的數據包根據 IP 標頭具有 20 字節的 TCP 負載數據,並且 TCP 負載也用於校驗和計算。 在我的示例中,我只使用了 TCP 標頭,沒有任何其他有效負載。

代碼中的問題

在提供的代碼中發現了許多問題。

tcp_checksum()

  1. 此函數計算 IPv4 校驗和。 要針對 IPv6 修改它,您需要將計算中使用的 IP 地址大小從 4 字節擴展到 16 字節。

  2. ip_srcip_dst初始化周圍的代碼是錯誤的,應該是:


    uint16_t *ip_src=(uint16_t *)&src_addr->in_addr;
    uint16_t *ip_dst=(uint16_t *)&dest_addr->in_addr;

get_ipv6_udptcp_checksum()

l4_len不是從網絡字節順序轉換的。 它應該是:

l4_len = ntohs(ipv6_hdr->ip6_plen);

主要的()

計算的校驗和不會轉換為網絡字節順序,因為它應該是:

tcphdr.th_sum = htons(get_ipv6_udptcp_checksum(&iphdr, (uint16_t *)&tcphdr));

帶有偽標頭的 UDP 幀的校驗和計算和:已解決! (包括 C 代碼、示例輸出和用於驗證的 Wireshark 數據幀)

你好,我一直在研究同樣的問題:請找到一個 C 程序,它根據偽標頭和幀內容生成正確的 UDP CHECK。 包括程序輸出和現實生活中同一幀的wireshark結果。

unsigned long sum=0, sum2=0, term=0;
CString s;

#define DATABYTES 3

// pseudo header bytes:
unsigned char ip1[]={192,168,11,25};   // ip 1
unsigned char ip2[]={192,168,11,20};  // ip 2
unsigned char pr []= {0,17};         // udp protocol 
unsigned char len[]= {0,8+DATABYTES};         // UDP entire message length entire 

// acutal message bytes:
unsigned char port1[] ={0x22,0xb8};   // port 1
unsigned char port2[] ={0x22,0xb8};   // port 1
unsigned char msglen[]={0,8+DATABYTES};         // UDP entire message length entire 
unsigned char check[] = {0,0};        // check set to 0 
unsigned char msg[DATABYTES]= {0x11,0x22,0x33};         // 10 bytes message length

// add artificial pseudo header bytes to sum
term = ((unsigned short) ip1[0] << 8) + ip1 [1];
sum+=term;
s.Format("Add ip1 = 0x%04x Sum = 0x%04x\n",term,sum);  AfxTrace(s);
term = ((unsigned short) ip1[2] << 8) + ip1 [3];
sum+=term;
s.Format("Add ip1 = 0x%04x Sum = 0x%04x\n",term,sum);  AfxTrace(s);

term = ((unsigned short) ip2[0] << 8) + ip2 [1];
sum+=term;
s.Format("Add ip2 = 0x%04x Sum = 0x%04x\n",term,sum);  AfxTrace(s);
term = ((unsigned short) ip2[2] << 8) + ip2 [3];
sum+=term;
s.Format("Add ip2 = 0x%04x Sum = 0x%04x\n",term,sum);  AfxTrace(s);

term = ((unsigned short) pr[0] << 8) + pr [1];
sum+=term;
s.Format("Add pr = 0x%04x Sum = 0x%04x\n",term,sum);  AfxTrace(s);
term = ((unsigned short) len[0] << 8) + len [1];
sum+=term;
s.Format("Add len = 0x%04x Sum = 0x%04x\n",term,sum);  AfxTrace(s);


// ----------------------------------------------------------------
// add real udp header bytes to sum (ports ....)
term = ((unsigned short) port1[0] << 8) + port1[1];
sum+=term;
s.Format("Add port1 = 0x%04x Sum = 0x%04x\n",term,sum);  AfxTrace(s);
term = ((unsigned short) port2[0] << 8) + port2[1];
sum+=term;
s.Format("Add port2 = 0x%04x Sum = 0x%04x\n",term,sum);  AfxTrace(s);
term = ((unsigned short) msglen[0] << 8) + msglen [1];
sum+=term;
s.Format("Add msglen = 0x%04x Sum = 0x%04x\n",term,sum);  AfxTrace(s);

// ----------------------------------------------------------------
// add data bytes info:
for (int i=0;i<DATABYTES/2;i++)
{
    // add message data bytes to sum sum (ports ....)
    term = ((unsigned short) msg[i*2] << 8) + msg [1+i*2];
    sum+=term;
    s.Format("Add msg = 0x%04x sum = 0x%04x\n",term,sum);  AfxTrace(s);
}

if (DATABYTES % 2 != 0)
{
    term = ((unsigned short) msg[DATABYTES-1] << 8);
    sum+=term;
    s.Format("Add msg = 0x%04x sum = 0x%04x\n",term,sum);  AfxTrace(s);
}

sum= (sum >> 16) + (sum & 0xFFFF);
s.Format("Clean sum = 0x%04x \n",sum);  AfxTrace(s);

sum= (sum >> 16) + (sum & 0xFFFF);
s.Format("Clean sum = 0x%04x \n",sum);  AfxTrace(s);

sum= (~sum & 0xFFFF);
s.Format("not sum = 0x%04x \n",sum);  AfxTrace(s);

程序的輸出:

Add ip1 = 0xc0a8 Sum = 0xc0a8
Add ip1 = 0x0b19 Sum = 0xcbc1
Add ip2 = 0xc0a8 Sum = 0x18c69
Add ip2 = 0x0b14 Sum = 0x1977d
Add pr = 0x0011 Sum = 0x1978e
Add len = 0x000b Sum = 0x19799
Add port1 = 0x22b8 Sum = 0x1ba51
Add port2 = 0x22b8 Sum = 0x1dd09
Add msglen = 0x000b Sum = 0x1dd14
Add msg = 0x1122 sum = 0x1ee36
Add msg = 0x3300 sum = 0x22136
Add msg = 0x0000 sum = 0x22136
Add msg = 0x0000 sum = 0x22136
Clean sum = 0x2138 
Clean sum = 0x2138 
not sum = 0xdec7  

和wireshark框架:

Ethernet II, Src: Intel_af:ef:6f (00:03:47:af:ef:6f), Dst: aa:aa:bb:bb:cc:cd (aa:aa:bb:bb:cc:cd)
    Destination: aa:aa:bb:bb:cc:cd (aa:aa:bb:bb:cc:cd)
        Address: aa:aa:bb:bb:cc:cd (aa:aa:bb:bb:cc:cd)
        .... ..1. .... .... .... .... = LG bit: Locally administered address (this is NOT the factory default)
        .... ...0 .... .... .... .... = IG bit: Individual address (unicast)
    Source: Intel_af:ef:6f (00:03:47:af:ef:6f)
        Address: Intel_af:ef:6f (00:03:47:af:ef:6f)
        .... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
        .... ...0 .... .... .... .... = IG bit: Individual address (unicast)
    Type: IP (0x0800)
Internet Protocol Version 4, Src: 192.168.11.25 (192.168.11.25), Dst: 192.168.11.20 (192.168.11.20)
    Version: 4
    Header length: 20 bytes
    Differentiated Services Field: 0x00 (DSCP 0x00: Default; ECN: 0x00: Not-ECT (Not ECN-Capable Transport))
        0000 00.. = Differentiated Services Codepoint: Default (0x00)
        .... ..00 = Explicit Congestion Notification: Not-ECT (Not ECN-Capable Transport) (0x00)
    Total Length: 31
    Identification: 0xebe3 (60387)
    Flags: 0x00
        0... .... = Reserved bit: Not set
        .0.. .... = Don't fragment: Not set
        ..0. .... = More fragments: Not set
    Fragment offset: 0
    Time to live: 128
    Protocol: UDP (17)
    Header checksum: 0xb76c [correct]
        [Good: True]
        [Bad: False]
    Source: 192.168.11.25 (192.168.11.25)
    Destination: 192.168.11.20 (192.168.11.20)
    [Source GeoIP: Unknown]
    [Destination GeoIP: Unknown]
User Datagram Protocol, Src Port: ddi-udp-1 (8888), Dst Port: ddi-udp-1 (8888)
    Source port: ddi-udp-1 (8888)
    Destination port: ddi-udp-1 (8888)
    Length: 11
    Checksum: 0xdec7 [correct]
        [Good Checksum: True]
        [Bad Checksum: False]
Data (3 bytes)
    Data: 112233
    [Length: 3]

0000  aa aa bb bb cc cd 00 03 47 af ef 6f 08 00 45 00   ........G..o..E.
0010  00 1f eb e3 00 00 80 11 b7 6c c0 a8 0b 19 c0 a8   .........l......
0020  0b 14 22 b8 22 b8 00 0b de c7 11 22 33            .."."......"3

缺少一件事:如果輸出為 0000,則需要將其反轉為 FFFF 以發出計算檢查而不是不存在檢查的信號。 祝你好運,喬希。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM