A Go-based acoustic communication system
Published:
A network communication system demonstrating UDP-based DNS resolution and TCP-based HTTP requests over an acoustic physical layer using Go.
This project implements essential transport layer protocols (UDP and TCP) over sound waves, enabling real-world internet services like domain name resolution and web page retrieval through audio signals.
Table of Contents
- Table of Contents
- Overview
- Core Features
- Architecture
- Prerequisites
- Installation
- Environment Setup
- Usage
- Project Structure
- Implementation Details
- Configuration Parameters
- Troubleshooting
Overview
This project focuses on implementing transport layer protocols over acoustic communication. The main achievements are:
- UDP-based DNS Resolution: A complete DNS client that sends DNS queries using UDP and resolves domain names (e.g.,
www.example.com→93.184.216.34) - TCP-based HTTP Requests: A TCP client that establishes connections via 3-way handshake, sends HTTP GET requests, and receives web page content
- Acoustic Physical Layer: Uses sound waves (audio signals) as the transmission medium instead of electrical cables
The system enables a node behind a NAT gateway to access internet services through acoustic communication with another node that has internet connectivity.
Core Features
UDP DNS Resolution
The DNS implementation provides domain name resolution functionality:
Key Components:
- DNS Query Generation: Creates standard DNS query packets with proper format (DNS header, questions section, A record type)
- UDP Transport: Wraps DNS queries in UDP datagrams (port 53)
- Query ID Tracking: Assigns unique IDs to each query and tracks pending responses
- Response Parsing: Extracts IP addresses from DNS response packets
- Upstream DNS: Forwards queries to external DNS servers (1.1.1.1, 8.8.8.8)
- Timeout Handling: 5-second timeout for unresponsive queries
- Caching: Stores resolved domains for 5 minutes to reduce redundant queries
Workflow:
- User executes
ping www.example.com - Node 1 creates DNS query packet with unique ID
- Query wrapped in UDP datagram and sent via acoustic link to Node 2
- Node 2 (NAT gateway) forwards query to internet DNS server (1.1.1.1)
- DNS response received from internet
- Node 2 sends response back through acoustic link
- Node 1 extracts IP address and proceeds with ping/curl
TCP HTTP Implementation
The TCP implementation enables reliable HTTP communication:
Key Components:
- 3-Way Handshake: Complete SYN → SYN-ACK → ACK connection establishment
- State Machine: Tracks connection states (CLOSED → SYN_SENT → ESTABLISHED → FIN_WAIT)
- Sequence Numbers: Maintains proper sequence and acknowledgment numbers
- Data Transmission: Sends HTTP GET requests with PSH+ACK flags
- Reliable Delivery: Handles ACKs and retransmissions
- Connection Teardown: FIN handshake for clean connection closure
- HTTP Request Generation: Constructs valid HTTP/1.0 GET requests
- Response Buffering: Accumulates TCP segments into complete HTTP response
Workflow:
- User executes
curl www.example.com - Node 1 resolves domain to IP (93.184.216.34) via DNS
- Node 1 initiates TCP connection:
- Sends SYN packet through acoustic link
- Node 2 forwards to internet server
- Receives SYN-ACK from server via Node 2
- Sends ACK to complete handshake
- Node 1 sends HTTP GET request in PSH+ACK segment
- Node 2 forwards request to web server
- Web server responds with HTTP headers and HTML content
- Node 2 forwards response segments back through acoustic link
- Node 1 reassembles segments and displays HTTP response
- Connection closed with FIN handshake
Lower Layers (Brief)
To support UDP and TCP over acoustic communication, several lower layers are implemented:
Physical Layer (src/phy/):
- Chirp Preamble: 2-10kHz frequency sweep for frame synchronization
- Manchester Encoding: Line coding for reliable bit transmission
- CRC-16: Error detection for frame integrity
- JACK Audio: 48kHz sample rate audio I/O
Data Link Layer (src/data_link/): [code]
- CSMA/CA: Carrier sense multiple access with collision avoidance
- ACK Frames: Acknowledgment frames for received data
- Sliding Window: Selective repeat ARQ for reliable delivery
Frame Format: [Preamble Header Payload CRC]
Network Layer (src/ip/): [code]
- IP Routing: Basic IPv4 packet forwarding
- ICMP: Ping functionality (echo request/reply)
- NAT: Network Address Translation for internet access
These layers provide the foundation for reliable packet delivery, you may check our previous repo’s code.
Architecture
┌─────────────────────────────────────────────────────────┐
│ Application Layer │
│ [HTTP Requests, DNS Queries] │
│ ↑ │
│ curl www.example.com │
│ ping www.google.com │
├─────────────────────────────────────────────────────────┤
│ Transport Layer │
│ [TCP: 3-way handshake, reliable delivery] │
│ [UDP: DNS queries to port 53] │
├─────────────────────────────────────────────────────────┤
│ Network Layer │
│ [IP Routing, NAT, ICMP] │
├─────────────────────────────────────────────────────────┤
│ Data Link Layer │
│ [CSMA/CA, Framing, ACK] │
├─────────────────────────────────────────────────────────┤
│ Physical Layer │
│ [Chirp, Manchester, CRC] │
│ JACK Audio Server (48kHz) │
└─────────────────────────────────────────────────────────┘
Prerequisites
- Go 1.25+: Install from golang.org
- JACK Audio Server: Required for audio I/O
- Windows: Download from jackaudio.org
- Install ASIO4ALL driver for low-latency audio
- Network Requirements:
- Microsoft KM-TEST Loopback Adapter
- Audio cables or speakers/microphones for acoustic coupling
Installation
Clone the repository:
Install Go dependencies:
go mod download
Environment Setup
1. Start JACK Server
cd "C:\Program Files\JACK2\"
.\jackd.exe -S -X winmme -dportaudio -d"ASIO::ASIO4ALL v2" -r48000 -p128
Parameters:
-r48000: Sample rate of 48kHz-p128: Buffer size of 128 samples (adjust for latency/stability trade-off)
2. Install Microsoft Loopback Adapter
- Open Device Manager
- Click Action → Add legacy hardware
- Select Install the hardware that I manually select from a list
- Select Network adapters
- Select Microsoft as manufacturer
- Select Microsoft KM-TEST Loopback Adapter
- Complete installation
3. Configure Network Interfaces
Configure static IP addresses for the Loopback Adapter:
- Node 1 (Client):
172.18.0.1/255.255.255.0 - Node 2 (Gateway):
172.18.0.2/255.255.255.0
Configure Gateway on Node 1:
- Set default gateway:
172.18.0.2 - Set DNS server:
8.8.8.8
4. Get Network Interface Name
# List all network interfaces
go run cmd\network_test\main.go list
# Interface name format: \Device\NPF_{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}
Usage
DNS Query Example
Node 1 (Client):
go run cmd\node\main.go 1
Node 2 (NAT Gateway):
go run cmd\node\main.go 2
Execute DNS query on Node 1:
# In Node 1's console
ping www.google.com -n 4
Expected output:
[Node 1] Resolving domain: www.google.com
[DNS] Created query for www.google.com (ID: 1)
[Node 1] DNS query sent via acoustic link
[Node 2] Received DNS query, forwarding to 1.1.1.1
[Node 1] DNS response received: www.google.com -> 142.250.185.68
Pinging www.google.com [142.250.185.68] with 32 bytes of data:
Reply from 142.250.185.68: time=350ms
Reply from 142.250.185.68: time=345ms
...
HTTP Request Example
Execute HTTP request on Node 1:
# In Node 1's console
curl example.com
Expected output:
[Node 1] Resolving domain: www.example.com
[DNS] Created query for www.example.com (ID: 2)
[Node 1] Resolved www.example.com to 93.184.216.34
[TCP] Initiating connection to 93.184.216.34:80
[TCP] Sending SYN (Seq=0x12345678)
[TCP] Received SYN-ACK (Seq=XXXXXXX, Ack=0x12345679)
[TCP] Connection ESTABLISHED
[Curl] Connected! Sending HTTP Request...
[TCP] Sending HTTP GET request
--- HTTP Response ---
HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 1256
<!doctype html>
<html>
<head>
<title>Example Domain</title>
...
</head>
<body>
<h1>Example Domain</h1>
<p>This domain is for use in illustrative examples...</p>
</body>
</html>
---------------------
[TCP] Closing connection
[TCP] Sent FIN
Project Structure
acoustic-link/
├── cmd/
│ ├── network_test/ # Network interface testing
│ │ └── main.go
│ └── node/ # Main application
│ ├── main.go # Node logic, DNS/TCP/HTTP handlers
│ ├── reception.go # Acoustic signal reception
│ └── transmission.go # Acoustic signal transmission
├── src/
│ ├── transport/ # ★ Transport Layer (UDP, TCP, DNS)
│ │ ├── udp.go # UDP packet creation and parsing
│ │ ├── dns.go # DNS client: query generation, response parsing
│ │ └── tcp.go # TCP client: 3-way handshake, state machine
│ ├── ip/ # Network layer (IP routing, NAT, ICMP)
│ │ ├── router.go
│ │ └── icmp.go
│ ├── data_link/ # Data link layer (CSMA/CA MAC)
│ │ └── mac.go
│ ├── phy/ # Physical layer (chirp, CRC, Manchester)
│ │ ├── chirp.go
│ │ ├── crc.go
│ │ ├── frame.go
│ │ └── linecoding.go
│ ├── encode/ # Signal encoding
│ │ └── encoder.go
│ ├── decode/ # Signal decoding
│ │ └── decoder.go
│ └── utils/ # Utilities
│ └── utils.go
├── docs/ # Documentation
│ └── report.tex
├── go.mod
└── README.md
Key Files for UDP/DNS and TCP/HTTP:
- src/transport/dns.go: DNS query/response handling
- src/transport/udp.go: UDP packet creation
- src/transport/tcp.go: TCP state machine and reliability
- cmd/node/main.go: Application logic (RunCurl, RunPing)
Implementation Details
UDP and DNS Implementation
UDP Packet Creation (src/transport/udp.go):
func CreateUDPPacket(srcIP, dstIP net.IP, srcPort, dstPort uint16, payload []byte) ([]byte, error) {
// 1. Create IPv4 layer
ipLayer := &layers.IPv4{
Version: 4,
TTL: 64,
SrcIP: srcIP,
DstIP: dstIP,
Protocol: layers.IPProtocolUDP,
}
// 2. Create UDP layer
udpLayer := &layers.UDP{
SrcPort: layers.UDPPort(srcPort),
DstPort: layers.UDPPort(dstPort),
}
// 3. Set network layer for checksum calculation
udpLayer.SetNetworkLayerForChecksum(ipLayer)
// 4. Serialize layers: [IP][UDP][Payload]
gopacket.SerializeLayers(buffer, opts, ipLayer, udpLayer, gopacket.Payload(payload))
}
DNS Query Generation (src/transport/dns.go):
func (ds *DNSServer) CreateDNSQuery(domain string) ([]byte, uint16, error) {
// Assign unique query ID
queryID := ds.queryID
ds.queryID++
// Build DNS query packet
dns := &layers.DNS{
ID: queryID,
QR: false, // 0 = query
OpCode: layers.DNSOpCodeQuery,
RD: true, // Recursion desired
Questions: []layers.DNSQuestion,
}
// Serialize DNS packet
return buffer.Bytes(), queryID, nil
}
DNS Response Handling (src/transport/dns.go):
func (ds *DNSServer) HandleDNSResponse(payload []byte) {
// 1. Parse DNS response
ip, queryID, err := ds.ParseDNSResponse(payload)
// 2. Find matching pending query
ds.mu.Lock()
if ch, ok := ds.pendingQuery[queryID]; ok {
ch <- ip // Send resolved IP to waiting goroutine
}
ds.mu.Unlock()
}
func (ds *DNSServer) WaitForResponse(queryID uint16) (net.IP, error) {
ch := make(chan net.IP, 1)
ds.pendingQuery[queryID] = ch
// Wait for response with timeout
select {
case ip := <-ch:
return ip, nil
case <-time.After(5 * time.Second):
return nil, fmt.Errorf("DNS timeout")
}
}
Complete DNS Workflow:
// 1. Node 1 creates DNS query
dnsPayload, queryID := dnsServer.CreateDNSQuery("www.example.com")
// 2. Wrap in UDP packet
udpPacket := CreateUDPPacket(node1IP, node2IP, 55123, 53, dnsPayload)
// 3. Send through acoustic link to Node 2
node.QueueIPFrame(node2IP, udpPacket)
// 4. Node 2 receives and forwards to internet DNS (1.1.1.1:53)
router.ForwardToInternet(udpPacket)
// 5. Wait for response
ip := dnsServer.WaitForResponse(queryID)
// 6. Use resolved IP for subsequent operations
fmt.Printf("Resolved: %s -> %s\n", domain, ip)
TCP and HTTP Implementation
TCP State Machine (src/transport/tcp.go):
type TCPClient struct {
SrcIP, DstIP net.IP
SrcPort, DstPort layers.TCPPort
SeqNum, AckNum uint32
State string // "CLOSED", "SYN_SENT", "ESTABLISHED", "FIN_WAIT"
DataBuffer bytes.Buffer
SendIPPacket func(destIP net.IP, payload []byte)
}
// 3-Way Handshake
func (c *TCPClient) Connect() error {
// 1. Send SYN
c.State = "SYN_SENT"
c.sendSegment(true, false, false, nil) // SYN=true, ACK=false, FIN=false
// 2. Wait for SYN-ACK (handled in HandlePacket)
// 3. Send ACK (handled in HandlePacket when SYN-ACK received)
// Block until ESTABLISHED
for c.State != "ESTABLISHED" {
c.cond.Wait()
}
return nil
}
TCP Packet Handling (src/transport/tcp.go):
func (c *TCPClient) HandlePacket(ip *layers.IPv4, tcp *layers.TCP) {
switch c.State {
case "SYN_SENT":
// Expecting SYN-ACK
if tcp.SYN && tcp.ACK {
c.AckNum = tcp.Seq + 1 // ACK server's SYN
c.SeqNum = tcp.Ack // Update our sequence number
c.State = "ESTABLISHED"
// Send final ACK to complete handshake
c.sendSegment(false, true, false, nil) // SYN=false, ACK=true
c.cond.Broadcast() // Wake up Connect()
}
case "ESTABLISHED":
// Handle incoming data
if len(tcp.Payload) > 0 {
// Check sequence number (in-order delivery)
if tcp.Seq == c.AckNum {
c.DataBuffer.Write(tcp.Payload) // Accumulate HTTP response
c.AckNum += uint32(len(tcp.Payload))
c.sendSegment(false, true, false, nil) // Send ACK
}
}
// Handle FIN (server closing)
if tcp.FIN {
c.AckNum++
c.sendSegment(false, true, false, nil) // ACK the FIN
}
}
}
HTTP Request Generation (cmd/node/main.go):
func (n *Node) RunCurl(domain string) {
// 1. Resolve domain name
n.SendDNSQuery(domain, func(resolvedIP net.IP) {
// 2. Create TCP connection
srcPort := uint16(rand.Intn(10000) + 50000)
tcpClient := transport.NewTCPClient(
n.ipAddr, // 172.18.0.1
resolvedIP, // 93.184.216.34 (example.com)
srcPort, // Random port (e.g., 55123)
80, // HTTP port
0x12345678, // Initial sequence number
n.QueueIPFrame, // Send function
)
// 3. Establish TCP connection (3-way handshake)
if err := tcpClient.Connect(); err != nil {
fmt.Printf("Connection failed: %v\n", err)
return
}
// 4. Send HTTP GET request
httpRequest := fmt.Sprintf(
"GET / HTTP/1.0\r\n"+
"Host: %s\r\n"+
"User-Agent: Aethernet\r\n"+
"\r\n",
domain,
)
tcpClient.Write([]byte(httpRequest)) // Sends as PSH+ACK segment
// 5. Wait for HTTP response (15 second timeout)
time.Sleep(15 * time.Second)
// 6. Display accumulated response
fmt.Println("\n--- HTTP Response ---")
fmt.Println(tcpClient.Read()) // Read from DataBuffer
fmt.Println("---------------------")
// 7. Close TCP connection
tcpClient.Close() // Sends FIN
})
}
TCP Segment Serialization:
func (c *TCPClient) sendSegment(syn, ack, fin bool, data []byte) {
// Create TCP layer
tcp := &layers.TCP{
SrcPort: c.SrcPort,
DstPort: c.DstPort,
Seq: c.SeqNum,
Ack: c.AckNum,
SYN: syn,
ACK: ack,
FIN: fin,
PSH: len(data) > 0, // Push flag if sending data
Window: 65535,
}
// Create IP layer
ip := &layers.IPv4{
SrcIP: c.SrcIP,
DstIP: c.DstIP,
Protocol: layers.IPProtocolTCP,
TTL: 64,
}
// Set network layer for TCP checksum
tcp.SetNetworkLayerForChecksum(ip)
// Serialize: [IP][TCP][HTTP Data]
gopacket.SerializeLayers(buffer, opts, ip, tcp, gopacket.Payload(data))
// Send through lower layers
c.SendIPPacket(c.DstIP, buffer.Bytes())
}
Configuration Parameters
Edit cmd/node/main.go to adjust:
const (
// Transport layer
TCPTimeOut = 15 * time.Second // TCP response timeout
CloudflareDNS = "1.1.1.1" // Upstream DNS server
// Data link layer
RTTTimeout = 350 * time.Millisecond // ACK timeout
MaxRetries = 2 // Max retransmission attempts
// Network layer
GatewayMAC = "00-00-5e-00-01-01" // Gateway MAC address
)
Troubleshooting
DNS Resolution Fails
- Verify Node 2 has internet connectivity
- Check upstream DNS servers (1.1.1.1, 8.8.8.8) are accessible
- Increase DNS timeout if network is slow
- Check firewall allows outbound UDP port 53
TCP Connection Timeout
- Verify DNS resolution succeeded first
- Check Node 2’s NAT is properly forwarding TCP packets
- Increase TCPTimeOut if remote server is slow
- Verify destination server is reachable (try direct ping first)
HTTP Response Incomplete
- Increase TCPTimeOut to allow more time for response
- Check acoustic link quality (reduce noise, adjust volume)
- Verify TCP sequence numbers are correct
- Check for packet loss in lower layers
No Audio I/O
- Ensure JACK server is running
- Check audio device configuration
- Verify audio cables are connected
- Test with simple ping first before trying DNS/HTTP
High Packet Loss
- Reduce ambient noise
- Adjust microphone/speaker volume
- Increase buffer size in JACK configuration
- Use shorter cable connections when possible
