Dindin Wang
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee
    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee
  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    # 透過 Netfilter 自動過濾廣告 儘管我們可在網頁瀏覽器中透過像是 [AdBlock](https://getadblock.com/zh_TW/) 這類的 extension 來過濾廣告,但需要額外的設定和佔用更多系統資源,倘若我們能透過 [netfilter](https://www.netfilter.org/),直接在核心層級過濾網路廣告,那所有應用程式都有機會受益。 * 參考資訊: * [How to drop 10 million packets per second](https://blog.cloudflare.com/how-to-drop-10-million-packets/) * [Use the iptables firewall to block ads on your Linux machine](https://www.securitronlinux.com/debian-testing/use-the-iptables-firewall-to-block-ads-on-your-linux-machine/) * [nBlock](https://github.com/notracking/nBlock) * [2020 年開發紀錄](https://hackmd.io/@ZhuMon/2020q1_final_project) * 執行人: ItisCaleb → [開發紀錄](https://hackmd.io/@sysprog/BJb0NRYH3) ## TODO * 重現去年 Netfilter 實驗,記錄下來 >TLS ; https ; layer 4 firewall ; OSI 七層 TCP 3-way handshake SYN, sliding window * 閱讀 [深入理解 iptables 與 netfilter 架構](https://arthurchiao.art/blog/deep-dive-into-iptables-and-netfilter-arch-zh/) & [基於 iptables 做 NAT](https://arthurchiao.art/blog/nat-zh/) ## 實作 ### 手動更改 iptable 以過濾廣告 實驗前清除所有規則 ``` $ sudo iptables -F ``` 使用命令查找目標網頁的資訊 ```shell $ nslookup youtube.com Server: 127.0.0.53 Address: 127.0.0.53#53 Non-authoritative answer: Name: youtube.com Address: 142.251.42.238 Name: youtube.com Address: 2404:6800:4012:2::200e ``` 將`youtube.com`加入到 iptable 的 reject 名單 ``` $ sudo iptables -A OUTPUT -d 142.250.0.0/15 -j REJECT ``` 使用以下指令查看 iptable 內容 ```shell $ sudo iptables -L Chain INPUT (policy ACCEPT) target prot opt source destination Chain FORWARD (policy ACCEPT) target prot opt source destination Chain OUTPUT (policy ACCEPT) target prot opt source destination REJECT all -- anywhere cg-in-f0.1e100.net/15 reject-with icmp-port-unreachable ``` REJECT 中指定了目標是`cg-in-f0.1e100.net/15` ,這可能不是 YouTube 所有的 IP 地址,因此瀏覽器還可以順利打開 拒絕所有可能是 YouTube 的 IP 地址 ```shell Chain OUTPUT (policy ACCEPT) target prot opt source destination REJECT all -- anywhere cg-in-f0.1e100.net/15 reject-with icmp-port-unreachable REJECT all -- anywhere ord38s04-in-f0.1e100.net/16 reject-with icmp-port-unreachable REJECT all -- anywhere 216-58-0-0.cpe.distributel.net/16 reject-with icmp-port-unreachable ``` 測試連接 : 連接至`youtube.com`失敗 ```shell aa860630@aa860630-ASUS-TUF-Dash-F15-FX516PM-FX516PM:~$ curl -v http://youtube.com * Trying 172.217.160.78:80... * connect to 172.217.160.78 port 80 failed: Connection refused * Trying 2404:6800:4012::200e:80... * Immediate connect fail for 2404:6800:4012::200e: Network is unreachable * Failed to connect to youtube.com port 80 after 12 ms: Connection refused * Closing connection 0 curl: (7) Failed to connect to youtube.com port 80 after 12 ms: Connection refused ``` ### 使用核心模組以過濾廣告 根據 [Use the iptables firewall to block ads on your Linux machine](https://www.securitronlinux.com/debian-testing/use-the-iptables-firewall-to-block-ads-on-your-linux-machine/) 可以使用上述方式手動更改 iptable,但如果使用核心模組來過濾廣告不僅更靈活,同時也可以提高瀏覽器性能,主要差別如下: * 使用核心模組過濾可以根據域名進行過濾,而不需要依賴固定的 IP 地址 * 核心模組過濾可以在解析階段就阻止廣告域名,從而防止任何針對這些域名的連接嘗試,而 iptables 規則只能在連接建立後起作用 * 在 DNS 層過濾,可以更早地阻止廣告內容的加載,可能提高瀏覽器性能。 Netfilter 是一個框架,嵌入在 Linux 核心中,用於執行各種封包的操作任務,包括網路地址轉換 (NAT) 和封包過濾。它通過幾個預定義的鉤子 (hook) 在封包傳輸過程中運行。以下是五個主要鉤子及其用途: 1. `NF_IP_PRE_ROUTING` 在做任何路由決策之前觸發,當作檢查和修改封包的初始點,適用於路由前的目標 NAT 和封包處理 1. `NF_IP_LOCAL_IN` 針對發往本地系統的封包, 允許檢查和修改那些要交給本機的入站封包 1. `NF_IP_FORWARD` 用於在系統中從一個網路介面轉發到另一個網路介面的封包,對應用轉發規則和過濾不屬於本地系統的封包非常重要 1. `NF_IP_POST_ROUTING` 在所有路由決策做出之後但在封包實際傳送之前觸發,通常用於源 NAT 和在封包傳送前的附加封包處理 1. `NF_IP_LOCAL_OUT` 針對本地系統生成的封包,在它們實際發出之前,提供修改出站封包、設置源 NAT 或應用其他出站過濾規則的機會 ![image](https://hackmd.io/_uploads/BkSrcrBIR.png) 圖片來源 [Netfilter -- Linux netfilter Hacking HOWTO: Netfilter Architecture](https://netfilter.org/documentation/HOWTO/netfilter-hacking-HOWTO-3.html) 參照 [adriver](https://github.com/Jongy/adriver),使用 Linux 核心模組,即可在核心裡頭註冊不同的 hook 來處理封包,為了在使用者瀏覽網站時屏蔽廣告,因此 hook 必須作用在進入本機之前,也就是 Local In 或 Pre Routing ```c static struct nf_hook_ops my_ops = { .hook = my_hook, .pf = NFPROTO_IPV4, .hooknum = NF_INET_PRE_ROUTING, .priority = NF_IP_PRI_FIRST, }; static int mod_init(void) { return nf_register_net_hook(&net, &my_ops); } static void mod_exit(void) { nf_unregister_net_hook(&net, &my_ops); } ``` **而 hook 函式需接受三個參數** * `priv`: 為一個私有指針,通常用於傳遞 hook 函式自身需要的私有資料 * `skb`: 是一個指向 Linux 中網路封包(socket buffer)的指針。在 netfilter 中,封包通過系統中的不同階段(如 INPUT、OUTPUT 等)時,可以被 hook 函式捕獲和處理。skb 對象包含了網路封包的所有信息,包括協定頭、有效資料等。 * `state`: 是一個指向 `nf_hook_state` 構體的指針,這個結構體提供了有關 hook 點和系統狀態的詳細信息 ```c static unsigned int my_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state) { ... ``` `my_hook` 函式主要由 `should_run_get_sfilter()` 與 `should_run_dns_sfilter()` 組成,兩個 funtion 的功用在於檢查並提取傳入的封包是否為 DNS 封包或 HTTP封包,並將其資料存儲到 `buf` 結構體中 `run_get_sfilters(&buf)` 與 `run_dns_sfilters(&buf)` 則將 `buf` 與預先設置好的 DNS 過濾規則或 HTTP 過濾規則做比對,若比對成功說明其封包包含與廣告相關的內容,因此在函式中將 `ret` 設置為 `NF_DROP`,即丟棄該封包 **hook function 的返回值共五個:** * `NF_DROP`丟棄封包 * `NF_ACCEPT`允許封包通過 * `NF_STOLEN`表示封包已被當前 hook 函式"偷走"。封包的所有權已經轉移到當前 hook 函式中,不在繼續沿Netfilter 處理路徑傳輸 * `NF_QUEUE`將封包送往 nfqueue * `NF_REPEAT`重新呼叫這個 hook function ```c should_run_dns_sfilter(skb, &buf); if (buf.data) { if (run_dns_sfilters(&buf)) { printk(KERN_INFO "dns filter matched, dropping"); // just drop here, don't REJECT ret = NF_DROP; goto done; } ret = NF_ACCEPT; goto done; } // buf.data is NULL, no need to free should_run_get_sfilter(skb, &buf); if (buf.data) { if (run_get_sfilters(&buf)) { printk(KERN_INFO "get filter matched, dropping"); ret = NF_DROP; goto done; } } done: ``` **從 UDP 通訊協定中獲取 DNS 資訊** 檢查傳入的網路封包(`skb`)是否為 DNS 封包需經過一連串條件審核,步驟如下: 1. `skb`是否不為空 1. `skb`是否包含 `ip_header` 1. `ip_header` 裡的協定是否為 UDP 通訊協定 1. `udp_header` 是否不為空 1. 確認埠號為 53 ```c static void should_run_dns_sfilter(struct sk_buff *skb, struct buf *buf) { struct iphdr *ip_header = NULL; struct udphdr *udp_header = NULL; if(!skb || !(ip_header = ip_hdr(skb))) { printk(KERN_INFO "Not IP"); return; } if(IPPROTO_UDP != ip_header->protocol) { printk(KERN_INFO "Not UDP"); return; } udp_header = udp_hdr(skb); if(!udp_header) { printk(KERN_INFO "Failed to get UDP header"); return; } if(ntohs(udp_header->dest) == 53) { dns_data = (unsigned char *)((unsigned char *)udp_header + sizeof(struct udphdr)); dns_data_len = ntohs(udp_header->len) - sizeof(struct udphdr); } ... } ``` 埠號 53 是用於 DNS(域名系統)服務的預設埠,且許多廣告和跟蹤服務使用 DNS 請求來進行域名解析,過濾埠號 53 上的數據包可以有效地阻止這些廣告和跟蹤請求 在確認某網路封包就是目標封包後,提取其 DNS 資訊,及下圖中的 Data。 ![image](https://hackmd.io/_uploads/HJBJC5IUR.png) `udp_header` 指標位置 + `udphdr` 長度,即為 Data 位置 ```c dns_data = (unsigned char *)((unsigned char *)udp_header + sizeof(struct udphdr)); dns_data_len = ntohs(udp_header->len) - sizeof(struct udphdr); ``` `dns_data` ,即 DNS Message Packet,用 16 進位法表示如下: `de b2 01 20 00 01 00 00 00 00 00 01 09 67 6f 6f 67 6c 65 61 64 73 01 67 0b 64 6f 75 62 6c 65 63 6c 69 63 6b 03 6e 65 74 0b 64 6c 69 6e 6b 72 6f 75 74 65 72 00 00 1c 00 01 00 00 29 04 b0 00 00` 前 12 個 bytes 為 DNS Header,分別代表: * ID(2bytes): 識別碼 * Flag(2bytes): 用來描述 DNS 訊息封包的功能 * QR: 0 表示此訊息是查詢,1 為回應訊息封包 * OP-code: 表示封包的類型。0 是標準查詢,1 是反向查詢,2 是伺服器狀態查詢,3-15 保留未用 * AA: 1 表示回應中的資料是授權資料,來自管轄的名稱伺服器 * TC: 1 表示資料過長被截短,因為超過了 UDP 封包的限制(512 Bytes) * RD: 1 表示遞迴查詢,0 表示反覆查詢。通常由查詢封包設定,回應封包會反映同樣的設定 * RA: 由回應的伺服器設定,1 表示支援遞迴 * R-code: 在回應封包中表示查詢結果。0 沒有錯誤,1 封包格式錯誤,2 伺服器錯誤,3 名稱錯誤,4 不支援此查詢類型,5 拒絕查詢 * Question Count(2bytes): 表示後面緊接著問題區段的數量 * Answer RR Count(2bytes): 表示後面緊接著答案區段中資源紀錄(Resource Record, RR)的數量 * Authority RR Count(2bytes): 權威區段的紀錄數量 * Addition RR Count(2bytes):此欄位為增加權威區段中紀錄的數量 ![image](https://hackmd.io/_uploads/rkC9aNOLC.png =60%x) Question Name 欄位存放所欲查詢的 FQDN 名稱,每一名稱長度不定,因此,此欄位的長度也不定。網域名稱的存放是以 ASCII 字元表示,最長限制在 64 個字元之內,名稱中的『.』(dot),並不表示出來,而以字元計數取代。譬如: `09 67 6f 6f 67 6c 65 61 64 73 01 67 0b 64 6f 75 62 6c 65 63 6c 69 63 6b 03 6e 65 74 0b 64 6c 69 6e 6b 72 6f 75 74 65 72` * Length: 9, Label: googleads * Length: 1, Label: g * Length: 11, Label: doubleclick * Length: 3, Label: net * Length: 11, Label: dlinkrouter * Null terminator: 00 Fully Qualified Domain Name: `googleads.g.doubleclick.net.dlinkrouter` 參考資料: [DNS 訊息格式](https://www.tsnien.idv.tw/Internet_WebBook/chap13/13-6%20DNS%20%E8%A8%8A%E6%81%AF%E6%A0%BC%E5%BC%8F.html) 封包在執行完 `should_run_dns_sfilter` 之後,若包含 DNS 資訊的話會將其存入 `buf->data` 裡,用來與 `dns_sfilters[]` 裡的字串做比對,若比對成功則丟棄封包,`dns_sfilters[]` 包含許多過濾規則,內容如下: ```c const struct sfilter dns_sfilters[] = { {3, {"media","admob","com" } }, {3, {"tpc","googlesyndication","com" } }, {3, {"pagead2","googlesyndication","com" } }, {3, {"xads","zedo","com" } }, {3, {"xads","zedo","com" } }, {3, {"ad","doubleclick","net" } }, {4, {"cm","g","doubleclick","net" } } } ``` 使用 `sudo dmesg` 查看核心訊息 ```shell [ 702.524716] buf data: googleadsgdoubleclicknet [ 702.524718] DNS filter matched [ 703.106169] buf data: lexicon33acrosscom [ 703.106172] DNS filter matched [ 702.973604] buf data: securepubadsgdoubleclicknet [ 702.973606] DNS filter matched ``` **從 TCP 通訊協定中獲取 HTTP 資訊** 檢查傳入的網路封包(`skb`)是否為 DNS 封包需經過一連串條件審核,步驟如下: 1. `skb`是否不為空 1. `skb`是否包含 `ip_header` 1. `ip_header` 裡的協定是否為 TCP 通訊協定 1. `tcp_header` 是否不為空 ```c static void should_run_get_sfilter(struct sk_buff *skb, struct buf *buf) { struct iphdr *iph = (struct iphdr *)skb_network_header(skb); struct tcphdr *tcp_header; if(!skb){ return; } if (!iph || iph->protocol != IPPROTO_TCP) { printk(KERN_INFO "Not a TCP packet\n"); return; } tcp_header = (struct tcphdr *) skb_transport_header(skb); if (!tcp_header) { printk(KERN_INFO "Failed to get TCP header\n"); return; } ... } ``` 我在解析 TCP 通訊協定的資料時,獲取 payload 的方式與 UDP 一樣,但印出來的都是以 `16 03 03` 或 `17 03 03` 為開頭且無法被閱讀的資料。我嘗試理解格式訊息發現資料使用 TLS 進行加密,因此無法被解析 > 16 03 03 * `16` TLS Handshake protocol * `03 03` SSL 3.3 (TLS 1.2) * `00 7a` Length of handshake message (122 bytes) > 17 03 03 * `17` TLS Application Data * `03 03` version 1.2 * `00 45` length of encrypted data (69 bytes) 得到 `tcp_header` 後,嘗試讀取其 Source port 及 Destination port ```shell [18156.615918] Source port: 443 [18156.615922] Destination port: 43550 [18156.615926] payload = 17 03 03 00 6a 85 d5 73 14 a2 23 72 ae be 80 82 ef 62 ae e9 27 f8 8b 03 38 b3 0e a5 a8 2b 59 e9 1c 74 6b d8 c7 a0 a8 0b 75 f2 6c 9b b3 fe 75 5f 57 ef 87 9a 19 cb 46 19 4d 73 ae 77 e8 75 8c 0d ``` 埠號為 443 說明這個 TCP 連接是用於 HTTPS 通訊,是 HTTP 協定的安全版本,通過 TLS/SSL 進行加密,確保資料在傳輸過程中受到保護,這也解釋上述 payload 無法被解析的原因 ### 透過掛載 module 實作 ```$ sudo insmod adblock.ko``` ```$ lsmod | grep adblock``` 瀏覽網頁會發現許多廣告被屏蔽了 ![image](https://hackmd.io/_uploads/rkIQJTY8C.png =90%x) 使用`ifconfig`查詢你的系統中有哪些網卡,並確定你要監聽的網卡名稱,須先下載套件`net-tools` ```shell ~~aa860630@aa860630-ASUS-TUF-Dash-F15-FX516PM-FX516PM:~/adriver$ ifconfig eno2: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500 ether 04:42:1a:d1:d5:20 txqueuelen 1000 (Ethernet) RX packets 0 bytes 0 (0.0 B) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 0 bytes 0 (0.0 B) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 lo: flags=73<UP,LOOPBACK,RUNNING> mtu 65536 inet 127.0.0.1 netmask 255.0.0.0 inet6 ::1 prefixlen 128 scopeid 0x10<host> loop txqueuelen 1000 (Local Loopback) RX packets 5280 bytes 495925 (495.9 KB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 5280 bytes 495925 (495.9 KB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 wlo1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 192.168.100.110 netmask 255.255.255.0 broadcast 192.168.100.255 inet6 fe80::9ff8:2596:4921:4e8f prefixlen 64 scopeid 0x20<link> ether 20:1e:88:e9:bb:1e txqueuelen 1000 (Ethernet) RX packets 140233 bytes 201059706 (201.0 MB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 56952 bytes 6138159 (6.1 MB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0~~ ``` `lo`顯示 running 使用此網路介面 ```shell sudo tcpdump -ni lo -c 10 -t udp and dst host 127.0.0.1 ```

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully